This commit is contained in:
Ilya Kantor 2017-04-17 14:20:01 +02:00
parent 1b03278014
commit 8134937fc3
12 changed files with 303 additions and 117 deletions

View file

@ -9,40 +9,38 @@ The most obvious example is `setTimeout`, but there are others, like making netw
## Callbacks
Consider a function `loadScript` that loads a script:
Consider this function `loadScript(src)` that loads a script:
```js
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
```
When the `<script>` tag is created and `src` is assigned, the browser loads the script and executes it. So, the function works. We can use it like this:
When the script element is added to the document, the browser loads it and executes. So, the function works.
We can use it like this:
```js
// loads and executes the script
loadScript('/my/script.js');
```
The function is asynchronous: the script starts loading now, but finishes later, maybe after a few seconds. So, the question is: how can we track the load end? As of now, the function provides no such way.
The function is asynchronous: the script starts loading now, but finishes later.
We'd like to invoke our code after the script is loaded. One of the easiest ways is to add a second argument to `loadScript`: the function that would run on load end.
```smart header="Synchronous vs asynchronous"
"Synchonous" and "asynchronous" are general programming terms, not specific to JavaScript.
A *synchronous* action suspends the execution until it's completed. For instance, `alert` and `prompt` are synchronous: the program may not continue until they are finished.
How can hook on "load completion"?
An *asynchronous* action allows the program to continue while it's in progress. For instance, `loadScript` in the example above initiates the script loading, but does not suspend the execution. Other commands may execute while the script is loading.
```
As of now, `loadScript` provides no way to track the load end. How can we execute our own code after the script is loaded?
From ancient times, Javascript allowed to use callback functions for asynchronous
Most asychronous
In this chapter we cover how to write callback-based asynchronous code.
Let's see a couple of examples, so that we can discover a problem, and then solve it using "promises".
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:
Let's allow that by adding a custom function as a second argument to `loadScript`, that should execute at that moment:
```js
function loadScript(src, callback) {
@ -63,25 +61,24 @@ loadScript('/my/script.js', function(script) {
});
```
...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.
...And it works, shows the `alert` after the script is loaded.
## Callback in callback
What if we need to load two scripts: one more after the first one?
What if we need to load two scripts sequentially: the first one, and then the second one after it?
We can put another `loadScript` inside the callback, like this:
We can put the second `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`);
});
*/!*
});
```
@ -96,11 +93,11 @@ loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
if (something) {
loadScript('/my/script3.js', function(script) {
*!*
loadScript('/my/script3.js', function(script) {
// ...continue after all scripts are loaded
*/!*
});
*/!*
}
})
@ -114,7 +111,7 @@ As you can see, a new asynchronous action means one more nesting level.
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:
Here's an improved version of `loadScript` that tracks loading errors:
```js run
function loadScript(src, callback) {
@ -138,32 +135,34 @@ loadScript('/my/script.js', function(error, script) {
if (error) {
// handle error
} else {
// script loaded, go on
// script loaded successfully
}
});
```
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.
The first argument of `callback` is reserved for errors, and the second argument is for the successful result.
## 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.
What we've just seen is called a "callback-based" approach to asynchronous programming. We pass a function, and it should run after the process is complete: with an error or a successful result.
But for multiple asynchronous actions that follow one after another...
From the first look it's a viable way of asynchronous coding. And indeed it is. For one or maybe two nested calls it looks fine.
But for multiple asynchronous actions that follow one after another we'll have a code like this:
```js
loadScript('/my/script1.js', function(error, script) {
loadScript('1.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('/my/script2.js', function(error, script) {
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('/my/script3.js', function(error, script) {
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
@ -180,35 +179,16 @@ loadScript('/my/script1.js', function(error, script) {
```
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 `(*)`.
1. We load `1.js`, then if there's no error.
2. We load `2.js`, then if there's no error.
3. We load `3.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.
As calls become more nested, the whole thing becomes 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.
The pyramid grows to the right with every asynchronous action. Soon it spirales out of control.
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.
Fortunately, there are ways to evade such pyramids. One of them is using "promises", we'll study them in the next chapters.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Before After
Before After

View file

@ -1,36 +1,41 @@
# 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) {
// ...
// executor
});
```
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.
The function passed to `new Promise` is called *executor*. It is called automatically and immediately when the promise is created.
At the end, it should call one of:
So, if you run this, the output is shown immediately:
```js run
let promise = new Promise(function(resolve, reject) {
alert(resolve); // function () { [native code] }
alert(reject); // function () { [native code] }
});
```
The string description of `resolve` and `reject` may differ between engines, but they both are functions provided by JavaScript itself. We don't need to create them.
The executor should do a job, like loading a script, and 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:
Like this:
```js
let promise = new Promise(function(*!*resolve*/!*, reject) {
// this function is executed automatically when the promise is constructed
setTimeout(() => *!*resolve("done!")*/!*, 1000);
// after 1 second signal that the job is done with the result "done!"
setTimeout(() => resolve("done!"), 1000);
});
```
@ -38,24 +43,31 @@ Or, in case of an error:
```js
let promise = new Promise(function(resolve, *!*reject*/!*) {
setTimeout(() => *!*reject(new Error("Woops!"))*/!*, 1000);
// after 1 second signal that the job is finished with an error
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":
## Promise state
The promise object has an internal state. The `Promise` constructor sets it to "pending". Then it may change in 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.
```smart
The promise that is either resolved or rejected is also called "settled" (as opposed to "pending").
```
The `promise` object provides the method `promise.then(onResolve, onReject)`.
The external code may add "handlers" to run when the promise becomes settled.
Both its arguments are functions:
There are two methods for that:
- `onResolve` is called when the state becomes "fulfilled" and gets the result.
- `onReject` is called when the state becomes "rejected" and gets the error.
- `promise.then(onResolve)` schedules the function `onResolve` to call when the promise is fulfilled, and it gets the result.
- `promise.catch(onReject)` schedules the function `onReject` to call when the promise is rejected, and it gets the error.
For instance, here `promise.then` outputs the result when it comes:
For instance, here's how to react on the successful job completion:
```js run
let promise = new Promise(function(resolve, reject) {
@ -66,8 +78,7 @@ let promise = new Promise(function(resolve, reject) {
promise.then(result => alert(result));
```
...And here it shows the error message:
...And here's the reacton on an error:
```js run
let promise = new Promise(function(resolve, reject) {
@ -75,28 +86,29 @@ let promise = new Promise(function(resolve, reject) {
});
// shows "Woops!" after 1 second
promise.then(null, error => alert(error.message));
promise.catch(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`:
Also we can call `promise.then(onResolve, onReject)` to set both handlers at once. Technically, `promise.catch(func)` works the same way as `promise.then(null, func)`.
```js run
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Woops!")), 1000);
setTimeout(() => resolve("done!"), 1000);
});
*!*
promise.catch(error => alert(error.message));
// set both success and error handlers
promise.then(
result => alert(result),
error => alert(error)
);
*/!*
```
"What's the benefit?" -- one might ask, "How do we use it?" Let's see an example.
So, when we want to do something asynchronously, we can create a promise with the proper executor to do a job, and then add handlers to it. They run when it finishes.
Let's see more examples to explore the benefits over the callback-based approach.
## Example: loadScript
@ -116,7 +128,7 @@ function loadScript(src, callback) {
Let's rewrite it using promises.
The call to `loadScript(src)` below returns a promise that settles when the loading is complete:
The call to `loadScript(src)` below returns a promise that resolves/rejects when the loading is complete:
```js run
function loadScript(src) {
@ -139,20 +151,24 @@ promise.then(
script => alert(`${script.src} is loaded!`),
error => alert(`Error: ${error.message}`);
);
promise.then(script => alert('One more handler to do something else with the script'));
```
The benefits compared to the callback syntax:
We can immediately see some benefits over the callback-based syntax in the example above:
- 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.
1. We can call `promise.then` as many times as want, so we can add any number of handlers.
2. We can call `promise.then` at any time. Maybe much later, when we really need that script.
3. The `promise` object can be passed around, new handlers can be added where needed in other parts of the code.
So promises already give us flexibility. But there's more. We can chain promises, see the next chapter.
So promises give us flexibility. But there's more. We can chain promises and use `async` functions and so on. We'll see that in the next chapters.
Read the notes below for better understanding of promise objects.
````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.
When either `resolve` or `reject` is called -- the state change is final. The argument of `resolve/reject` 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.
Future calls of `resolve/reject` are ignored, so there's no way to "re-resolve" or "re-reject" a promise.
For instance, here only the first `resolve` works:
@ -160,25 +176,58 @@ For instance, here only the first `resolve` works:
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
// a subsequent resolve is ignored
setTimeout(() => resolve("..."), 1000);
// a subsequent reject is ignored
setTimeout(() => reject(new Error("..."), 2000));
});
// 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.
````smart header="A promise may resolve immediately, doesn't have to be asynchronous"
As we've seen from the example above, a promise may call resolve/reject without delay:
When the promise is settled (resolved or rejected), subsequent `promise.then` callbacks are executed immediately.
```js run
new Promise(function(resolve, reject) {
resolve("done!"); // immediately fulfill with the result: "done"
}).then(alert);
```
That's normal, sometimes it turns out that there's no asynchronous job to be done. For instance, if we have a cached result.
````
````smart header="On settled promises `then` runs immediately"
We can add `.then/catch` at any time.
If the promise has not settled yet, then it will wait for the result.
Otherwise, if the promise has already resolved/rejected, then subsequent `promise.then/catch` handlers are executed immediately.
To be precise -- they are still asynchronous, but run as soon as possible, similar to `setTimeout(...,0)`.
For instance, here we'll first see "code end", and then "done!":
```js run
// the promise is in the "resolved" state immediately
let promise = new Promise(function(resolve, reject) {
resolve("done!");
});
// the handler runs as soon as possible, but asynchronously, like setTimeout(...,0)
promise.then(alert);
alert('code end');
```
````
````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`:
We can call them without any arguments too. The call `resolve()` makes the result `undefined`:
```js run
let promise = new Promise(function(resolve, reject) {
@ -188,17 +237,21 @@ let promise = new Promise(function(resolve, reject) {
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.
...But if we pass many 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
let name = "John";
let age = 25;
resolve({name, age}); // "pack" the values in an object
});
// destructuring the object into name and age variables
// destructuring
promise.then(function({name, age}) {
// here we have name and age variables as if the promise had two results
alert(`${name} ${age}`); // John 25
})
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Before After
Before After

View file

@ -1,18 +1,25 @@
# Promises chaining
There are many great things about promises. We're only starting.
Let's formulate the problem mentioned in the chapter <info:callback-hell>:
Now we'll cover promises chaining. They allow to build sequences of asynchronous actions.
- We have a sequence of tasks to be done one after another. For instance, loading scripts. The next task may need the result of the previous one.
- How to code it well?
[cut]
Promises can cover that need in two ways:
Here's the idea:
1. Promises chaining.
2. Async functions.
Let's see the first way in this chapter and the second one in the next.
Promises chaining looks like this:
```js run
new Promise(function(resolve, reject) {
// do a job...
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
@ -32,12 +39,132 @@ new Promise(function(resolve, reject) {
// ...
```
As you can see:
As we can see, a call to `promise.then` returns a promise, that we can use again for `.then`. A value returned by `.then` becomes a result in the next `.then`. So in the example above we have a sequence of results: `1` -> `2` -> `4`.
Please note that chaining `.then` is not the same as many `.then` on a single promise, like below:
```js run
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
```
In the code above, all `.then` are on the same promise, so all of them get the same result -- the result of that promise. And all `alert` show the same: 1.
If we want to use the value returned by a handler of `.then`, then we should add a new `.then` after it (to chain).
## Returning promises
Normally, the value returned by a handler is passed to the next `.then`. But there's an exception. If the returned value is a promise, then further execution is suspended till it settles. And then the result of that promise is used.
Let's see it in action here:
```js run
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
*!*
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
*/!*
}).then(function(result) {
alert(result); // 2
*!*
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
*/!*
}).then(function(result) {
alert(result); // 4
});
```
Now we have the same 1 -> 2 > 4 output, but with 1 second delay between each.
When we return `new Promise(…)`, the next `.then` in the chain is executed when it settles and gets its result.
Let's use it to `loadScript` multiple scripts one by one:
```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);
});
}
loadScript("/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("/article/promise-chaining/three.js");
})
.then(function(script) {
// use variables declared in scripts
// to show that they indeed loaded
alert("Done: " + (one + two + three));
});
```
The code totally evades the pyramid of doom. We can add more asynchronous actions to the chain, and the code is still "flat".
## Inheriting from promise, thenables, error handling?
An object that has a method called `.then` is called a "thenable".
Instead of checking if something is `instanceof Promise`, we should usually check it for being thenable, and if it is, then treat it as a promise ("duck typing").
JavaScript specification also checks the value returned by a handler for being a thenable, not exactly a promise, when it decides whether to pass it along the chain or wait for the result. So in the examples above we could use custom thenables instead of `Promise` instances.
For instance, native promises give no way to "abort" the execution. The `loadScript` above cannot "cancel" script loading, just because there's no `.abort` method on promises, we can only listen for the state change using `.then/catch`.
Let's
## Error handling
- Calls to `.then` can be chained -- that's because `promise.then` returns a promise.
- A value returned by `.then` becomes a result in the next `.then`.
If there's an error, it is also passed down the chain:
```js run

View file

@ -0,0 +1 @@
let one = 1;

View file

@ -0,0 +1 @@
let three = 3;

View file

@ -0,0 +1 @@
let two = 2;

23
archive/thenable.js Normal file
View file

@ -0,0 +1,23 @@
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);
});
}
loadScript("/article/promise-chaining/one.js")
.then(script => {
return {
then(resolve, reject) {
setTimeout(() => resolve(script), 1000);
}
};
})
.then(function(script) {
alert(one);
});

Binary file not shown.