Overview
The Promise object is JavaScript’s solution for asynchronous operations, providing a unified interface for asynchronous operations. It acts as a proxy, acting as an intermediary between asynchronous operations and callback functions, giving asynchronous operations the interface of synchronous operations. promise allows asynchronous operations to be written as if they were synchronous operations, without having to nest layers of callback functions.
Note that this chapter is only a brief introduction to the Promise object.
First, Promise is an object and a constructor.
function f1(resolve, reject) {
// Asynchronous code...
}
var p1 = new Promise(f1);
In the above code, the Promise
constructor accepts a callback function f1
as an argument, and inside f1
is the code for the asynchronous operation. Then, the returned p1
is a Promise instance.
The idea behind Promise is that all asynchronous tasks return a Promise instance, which has a then
method that specifies the next callback function.
var p1 = new Promise(f1);
p1.then(f2);
In the above code, f2
is executed when the execution of the asynchronous operation of f1
is completed.
The traditional way of writing f2
might require passing f2
into f1
as a callback function, for example, as f1(f2)
, and calling f2
inside f1
after the asynchronous operation completes. Promise makes f1
and f2
chain-written. Not only does this improve readability, but it is especially convenient for callback functions with multiple layers of nesting.
// The traditional way of writing
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// ...
});
});
});
});
// The way to write a Promise
(new Promise(step1))
.then(step2)
.then(step3)
.then(step4);
As you can see from the above code, the flow of the program becomes very clear and easy to read after using Promises. Note that the format of the Promise
instance generation in the above code has been simplified for easier understanding.
In general, the traditional way of writing callback functions makes the code muddled and grows horizontally rather than downwards. promise solves this problem by making asynchronous processes writeable as synchronous processes.
Promise was originally a community idea, and some libraries were the first to implement it. ecmascript 6 wrote it into the language standard, and now JavaScript natively supports Promise objects.
State of Promise objects
Promise objects control asynchronous operations through their own state, and Promise instances have three states.
- Asynchronous operations are not completed (pending)
- Asynchronous operation succeeded (fulfilled)
- Failed asynchronous operation (rejected)
Of the above three states, fulfilled
and rejected
together are called resolved
(finalized).
There are only two ways to change the status of these three.
- From “unfinished” to “successfully”
- From “incomplete” to “failed”
Once the state has changed, it is frozen and no new state changes. This is the origin of the name Promise, which means “promise” in English, and once a promise is made, it cannot be changed. This also means that a change in the state of a Promise instance can only happen once.
Therefore, there are only two end results of a Promise.
- If the asynchronous operation succeeds, the Promise instance passes back a value and the state changes to
fulfilled
. - If the asynchronous operation fails, the Promise instance throws an error and the state becomes
rejected
.
Promise Constructors
JavaScript provides a native Promise
constructor to generate Promise instances.
var promise = new Promise(function (resolve, reject) {
// ...
if (/* asynchronous operation succeeded */){
resolve(value);
} else { /* asynchronous operation failed */
reject(new Error());
}
});
In the code above, the Promise
constructor accepts a function as an argument, and the two arguments to that function are resolve
and reject
. They are two functions, provided by the JavaScript engine, that you don’t have to implement yourself.
The purpose of the resolve
function is to change the state of the Promise
instance from unfinished
to successful
(i.e., from pending
to fulfilled
), call it when the asynchronous operation succeeds, and The result of the asynchronous operation is passed as an argument. The reject
function changes the status of the Promise
instance from not completed
to failed
(i.e., from pending
to rejected
), is called when the asynchronous operation fails, and takes the The error reported by the asynchronous operation is passed as an argument.
Here is an example.
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100)
In the above code, timeout(100)
returns a Promise instance. after 100 milliseconds, the state of the instance will change to fulfilled
.
Promise.prototype.then()
The then
method of a Promise instance, used to add a callback function.
The then
method can accept two callback functions, the first one when the asynchronous operation succeeds (becomes fulfilled
state) and the second one when the asynchronous operation fails (becomes rejected
) (this parameter can be omitted). Once the state changes, the corresponding callback function is called.
var p1 = new Promise(function (resolve, reject) {
resolve('success');
});
p1.then(console.log, console.error);
// "success"
var p2 = new Promise(function (resolve, reject) {
reject(new Error('failed'));
});
p2.then(console.log, console.error);
// Error: Failure
In the above code, p1
and p2
are both Promise instances, and their then
methods bind two callback functions: console.log
on success, and console.error
on failure (which can be omitted). The state of p1
changes to success and the state of p2
changes to failure, and the corresponding callback function receives the value passed back from the asynchronous operation and outputs it on the console.
The then
method can be used in a chain.
p1
.then(step1)
.then(step2)
.then(step3)
.then(
console.log,
console.error
);
In the above code, p1
is followed by four then
s, which means that there are four callback functions in sequence. As soon as the status of the previous step changes to fulfilled
, the callback functions immediately following it will be executed in turn.
For the last then
method, the callback functions are console.log
and console.error
, with one important difference in usage. console.log
only shows the return value of step3
, while console.error
can show the error that occurred in any of p1
, step1
, step2
, and step3
. For example, if the status of step1
changes to rejected
, then neither step2
nor step3
will be executed (because they are resolved
callbacks). error`. This means that the Promise object’s error reporting is transitive.
then() Usage Analysis
The use of Promise is simply a matter of adding a callback function using the then
method. However, there are some subtle differences between the different ways of writing Promise, see the following four ways, what are the differences?
// Writing method one
f1().then(function () {
return f2();
});
// Write two
f1().then(function () {
f2();
});
// write method 3
f1().then(f2());
// write method 4
f1().then(f2);
For the sake of explanation, the following four ways of writing are all followed by a callback function f3
using the then
method. The argument to the f3
callback function in writeup one is the result of the f2
function.
f1().then(function () {
return f2();
}).then(f3);
The argument to the f3
callback function in writeup two is undefined
.
f1().then(function () {
f2();
return;
}).then(f3);
The argument to the f3
callback function in writeup three is the result of the run of the function returned by the f2
function.
f1().then(f2())
.then(f3);
There is one difference between writing method four and writing method one, and that is that f2
receives the result returned by f1()
.
f1().then(f2)
.then(f3);
Example: Image loading
Here’s how to load an image using Promise.
var preloadImage = function (path) {
return new Promise(function (resolve, reject) {
var image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
In the above code, image
is an instance of an image object. It has two event listening properties, onload
property is called when the image is loaded successfully and onerror
property is called when the load fails.
The preloadImage()
function above is used as follows.
preloadImage('https://example.com/my.jpg')
.then(function (e) { document.body.append(e.target) })
.then(function () { console.log('loaded successfully') })
In the above code, after the image is loaded successfully, the onload
property returns an event object, so the callback function of the first then()
method will receive this event object. The target
property of this object is the DOM node generated after the image is loaded.
Summary
The advantage of Promise is that it makes the callback function a canonical chain write, and the program flow can be seen clearly. It has a set of interfaces that enable many powerful features, such as executing multiple asynchronous operations at the same time, waiting until their states have changed, and then executing a single callback function; for example, specifying a uniform method for handling errors thrown by multiple callback functions, and so on.
Moreover, Promise has an advantage that traditional writing does not: once its state has changed, that state is available whenever it is queried. This means that whenever a callback function is added to a Promise instance, that function will be executed correctly. So you don’t have to worry about whether you missed an event or signal. If you write it conventionally, by listening for events to execute the callback function, once you miss the event, adding the callback function again won’t execute.
The downside of Promise is that it’s harder to write than the traditional way, and it’s not easy to read the code at a glance. You’ll just see a bunch of then
s and have to sort out the logic inside the then
callback function yourself.
Microtasks
Promise callback functions are asynchronous tasks that are executed after synchronous tasks.
new Promise(function (resolve, reject) {
resolve(1);
}).then(console.log);
console.log(2);
// 2
// 1
The above code will output 2 and then 1 because console.log(2)
is a synchronous task and the callback function of then
is an asynchronous task that must be executed later than the synchronous task.
However, Promise’s callback function is not a normal asynchronous task, but a microtask. The difference between them is that normal tasks are appended to the next event loop and microtasks are appended to the current event loop. This means that the microtask must be executed earlier than the normal task.
setTimeout(function() {
console.log(1);
}, 0);
new Promise(function (resolve, reject) {
resolve(2);
}).then(console.log);
console.log(3);
// 3
// 2
// 1
The output of the above code is 321
. This means that the callback function for then
is executed before setTimeout(fn, 0)
. Because then
is executed in the current event loop, setTimeout(fn, 0)
is executed at the beginning of the next event loop.