This commit is contained in:
Ilya Kantor 2017-04-07 00:20:56 +03:00
parent 2f4242c7bd
commit c96881755e
11 changed files with 408 additions and 0 deletions

View file

@ -0,0 +1,187 @@
# Callback hell
Many things that we do in JavaScript are asynchronous. We initiate a process, but it finishes later.
The most obvious example is `setTimeout`, but there are others, like making network requests, performing animations and so on.
Let's see a couple of examples, so that we can discover a problem, and then solve it using "promises".
[cut]
## Callbacks
Remember resource load/error events? They are covered in the chapter <info:onload-onerror>.
Let's say we want to create a function `loadScript` that loads a script and executes our code afterwards.
It can look like this:
```js
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(script);
document.head.append(script);
}
```
Now when we want to load a script and then do something, we can call:
```js
loadScript('/my/script.js', function(script) {
alert(`Cool, the ${script.src} is loaded, let's use it`);
});
```
...And it works, shows `alert` after the script is loaded.
That is called "a callback API". Our function `loadScript` performs an asynchronous task and we can hook on its completion using the callback function.
## Callback in callback
What if we need to load two scripts: one more after the first one?
We can put another `loadScript` inside the callback, like this:
```js
loadScript('/my/script.js', function(script) {
alert(`Cool, the ${script.src} is loaded, let's load one more`);
loadScript('/my/script2.js', function(script) {
alert(`Cool, the second script is loaded`);
});
});
```
Now after the outer `loadScript` is complete, the callback initiates the inner one.
...What if we want one more script?
```js
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
if (something) {
loadScript('/my/script3.js', function(script) {
*!*
// ...continue after all scripts are loaded
*/!*
});
}
})
});
```
As you can see, a new asynchronous action means one more nesting level.
## Handling errors
In this example we didn't consider errors. What if a script loading failed with an error? Our callback should be able to react on that.
Here's an improved version of `loadScript` that can handle errors:
```js run
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
*!*
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error ` + src));
*/!*
document.head.append(script);
}
```
It calls `callback(null, script)` for successful load and `callback(error)` otherwise.
Usage:
```js
loadScript('/my/script.js', function(error, script) {
if (error) {
// handle error
} else {
// script loaded, go on
}
});
```
The first argument of `callback` is reserved for errors and the second argument for the successful result. That allows to use a single function to pass both success and failure.
## Pyramid of doom
From the first look it's a viable code. And indeed it is. For one or maybe two nested calls it looks fine.
But for multiple asynchronous actions that follow one after another...
```js
loadScript('/my/script1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('/my/script2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('/my/script3.js', function(error, script) {
if (error) {
handleError(error);
} else {
*!*
// ...continue after all scripts are loaded (*)
*/!*
}
});
}
})
}
});
```
In the code above:
1. we load `/my/script1.js`, then if there's no error
2. we load `/my/script2.js`, then if there's no error
3. we load `/my/script3.js`, then if there's no error -- do something else `(*)`.
The nested calls become increasingly more difficult to manage, especially if we add real code instead of `...`, that may include more loops, conditional statements and other usage of loaded scripts.
That's sometimes called "callback hell" or "pyramid of doom".
![](callback-hell.png)
See? It grows right with every asynchronous action.
Compare that with a "regular" synchronous code.
Just *if* `loadScript` were a regular synchronous function:
```js
try {
// assume we get the result in a synchronous manner, without callbacks
let script = loadScript('/my/script.js');
// ...
let script2 = loadScript('/my/script2.js');
// ...
let script3 = loadScript('/my/script3.js');
} catch(err) {
handleError(err);
}
```
How much cleaner and simpler it is!
Promises allow to write asynchronous code in a similar way. They are really great at that. Let's study them in the next chapter.

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View file

@ -0,0 +1,206 @@
# Promise
There are many ways to explain promises. Here we'll follow the [specification](https://tc39.github.io/ecma262/#sec-promise-objects), because gives real understanding how things work. Besides, you'll become familiar with the terms.
First, what a promise is, and then usage patterns.
[cut]
## Promise objects
A promise is an object of the built-in `Promise` class. It has the meaning of the "delayed result".
The constructor syntax is:
```js
let promise = new Promise(function(resolve, reject) {
// ...
});
```
The function is called *executor*. It is called automatically when the promise is created and can do an asynchronous job, like loading a script, but can be something else.
At the end, it should call one of:
- `resolve(result)` -- to indicate that the job finished successfully with the `result`.
- `reject(error)` -- to indicate that an error occured, and `error` should be the `Error` object (technically can be any value).
For instance:
```js
let promise = new Promise(function(*!*resolve*/!*, reject) {
// this function is executed automatically when the promise is constructed
setTimeout(() => *!*resolve("done!")*/!*, 1000);
});
```
Or, in case of an error:
```js
let promise = new Promise(function(resolve, *!*reject*/!*) {
setTimeout(() => *!*reject(new Error("Woops!"))*/!*, 1000);
});
```
Initially, the promise is said to have a "pending" state. Then it has two ways. When `resolve` is called, the state becomes "fulfilled". When `reject` is called, the state becomes "rejected":
![](promiseInit.png)
The idea of promises is that an external code may react on the state change.
The `promise` object provides the method `promise.then(onResolve, onReject)`.
Both its arguments are functions:
- `onResolve` is called when the state becomes "fulfilled" and gets the result.
- `onReject` is called when the state becomes "rejected" and gets the error.
For instance, here `promise.then` outputs the result when it comes:
```js run
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});
// shows "done!" after 1 second
promise.then(result => alert(result));
```
...And here it shows the error message:
```js run
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Woops!")), 1000);
});
// shows "Woops!" after 1 second
promise.then(null, error => alert(error.message));
```
![](promise-resolve-reject.png)
- To handle only a result, we can use single argument: `promise.then(onResolve)`
- To handle only an error, we can use a shorthand method `promise.catch(onReject)` -- it's the same as `promise.then(null, onReject)`.
Here's the example rewritten with `promise.catch`:
```js run
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Woops!")), 1000);
});
*!*
promise.catch(error => alert(error.message));
*/!*
```
"What's the benefit?" -- one might ask, "How do we use it?" Let's see an example.
## Example: loadScript
We have the `loadScript` function for loading a script from the previous chapter, here's the callback-based variant:
```js
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error ` + src));
document.head.append(script);
}
```
Let's rewrite it using promises.
The call to `loadScript(src)` below returns a promise that settles when the loading is complete:
```js run
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
script.onload = () => resolve(script);
script.onerror = () => reject(new Error("Script load error: " + src));
document.head.append(script);
});
}
*!*
// Usage:
*/!*
let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js");
promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`);
);
```
The benefits compared to the callback syntax:
- We can add as many `.then` as we want and when we want. Maybe later.
- Also we can pass a promise object somewhere else, and new handlers can be added there, so that's extensible.
So promises already give us flexibility. But there's more. We can chain promises, see the next chapter.
````warn header="Once a promise settles, it can't be changed"
When either `resolve` or `reject` is called -- the promise becomes *settled* (fulfilled or rejected), and the state is final. The result is saved in the promise object.
Future calls of `resolve/reject` are ignored, there's no way to "re-resolve" or "re-reject" a promise.
For instance, here only the first `resolve` works:
```js run
let promise = new Promise(function(resolve, reject) {
resolve("done!"); // immediately fulfill with the result: "done"
setTimeout(() => resolve("..."), 1000); // ignored
setTimeout(() => reject(new Error("..."), 2000)); // ignored
});
// the promise is already fulfilled, so the alert shows up right now
promise.then(result => alert(result), () => alert("Never runs"));
````
```smart header="On settled promises `then` runs immediately"
As we've seen from the example above, a promise may call resolve/reject without delay. That happens sometimes, if it turns out that there's no asynchronous things to be done.
When the promise is settled (resolved or rejected), subsequent `promise.then` callbacks are executed immediately.
```
````smart header="Functions resolve/reject have only one argument"
Functions `resolve/reject` accept only one argument.
We can call them without any arguments: `resolve()`, that makes the result `undefined`:
```js run
let promise = new Promise(function(resolve, reject) {
resolve();
});
promise.then(result => alert(result)); // undefined
```
...But if we pass more arguments: `resolve(1, 2, 3)`, then all arguments after the first one are ignored. A promise may have only one value as a result/error.
Use objects and destructuring if you need to pass many values, like this:
```js run
let promise = new Promise(function(resolve, reject) {
resolve({name: "John", age: 25}); // two values packed in an object
});
// destructuring the object into name and age variables
promise.then(function({name, age}) {
alert(`${name} ${age}`); // John 25
})
```
````

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

View file

@ -0,0 +1,10 @@
<script>
function loadImage(src) {
return new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = src;
img.onload = () => resolve(img);
img.onerror = () => reject(new Error("Image load failed: " + src));
});
}
</script>

5
8-async/index.md Normal file
View file

@ -0,0 +1,5 @@
development: true
---
# Promises, async/await