minor
This commit is contained in:
parent
2f4242c7bd
commit
c96881755e
11 changed files with 408 additions and 0 deletions
187
8-async/01-callback-hell/article.md
Normal file
187
8-async/01-callback-hell/article.md
Normal 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".
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
BIN
8-async/01-callback-hell/callback-hell.png
Normal file
BIN
8-async/01-callback-hell/callback-hell.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
BIN
8-async/01-callback-hell/callback-hell@2x.png
Normal file
BIN
8-async/01-callback-hell/callback-hell@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
206
8-async/02-promise-basics/article.md
Normal file
206
8-async/02-promise-basics/article.md
Normal 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":
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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));
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
- 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
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
````
|
BIN
8-async/02-promise-basics/promise-init.png
Normal file
BIN
8-async/02-promise-basics/promise-init.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
8-async/02-promise-basics/promise-init@2x.png
Normal file
BIN
8-async/02-promise-basics/promise-init@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
8-async/02-promise-basics/promise-resolve-reject.png
Normal file
BIN
8-async/02-promise-basics/promise-resolve-reject.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
8-async/02-promise-basics/promise-resolve-reject@2x.png
Normal file
BIN
8-async/02-promise-basics/promise-resolve-reject@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
0
8-async/03-promise-chaining/article.md
Normal file
0
8-async/03-promise-chaining/article.md
Normal file
10
8-async/03-promise-chaining/head.html
Normal file
10
8-async/03-promise-chaining/head.html
Normal 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
5
8-async/index.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
development: true
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Promises, async/await
|
Loading…
Add table
Add a link
Reference in a new issue