Javascript Promise tutorial

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 thens, 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 thens 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.

Tags:

Javascript