Promise

Overview

Promise object is JavaScript's asynchronous operation solution, which provides a unified interface for asynchronous operation. It acts as a proxy, acting as an intermediary between asynchronous operations and callback functions, so that asynchronous operations have a synchronous operation interface. Promise allows asynchronous operations to be written, just like in the process of writing synchronous operations, without having to nest callback functions layer by layer.

Note that this chapter is just a brief introduction to Promise objects. In order to avoid repetition with subsequent tutorials, for a more complete introduction, please see "Promise Object" of "ES6 Standard Introduction" /#docs/promise) chapter.

First of all, Promise is an object and also 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 a parameter, and inside the f1 is the asynchronous operation code. Then, the returned p1 is a Promise instance.

The design idea of ​​Promise is that all asynchronous tasks return a Promise instance. The Promise instance has a then method to specify the callback function for the next step.

var p1 = new Promise(f1);
p1.then(f2);

In the above code, when the asynchronous operation of f1 is completed, f2 will be executed.

The traditional way of writing may need to pass f2 as a callback function to f1, such as f1(f2). After the asynchronous operation is completed, f2 is called inside f1. Promise makes f1 and f2 become chain writing. Not only improves readability, but it is especially convenient for multi-level nested callback functions.

// Traditional writing
step1(function (value1) {
  step2(value1, function (value2) {
    step3(value2, function (value3) {
      step4(value3, function (value4) {
        // ...
      });
    });
  });
});

// How to write Promise
new Promise(step1).then(step2).then(step3).then(step4);

As you can see from the above code, after adopting Promises, the program flow becomes very clear and easy to read. Note that in order to facilitate understanding, the generation format of the Promise instance of the above code has been simplified. Please refer to the following for the real syntax.

In general, the traditional way of writing callback functions makes the code mixed together and develops horizontally rather than downwards. Promise is to solve this problem, so that asynchronous processes can be written as synchronous processes.

Promise was originally an idea put forward by the community, and some function libraries were the first to implement this function. ECMAScript 6 writes it into the language standard, and currently JavaScript natively supports Promise objects.

The state of the Promise object

The Promise object controls asynchronous operations through its own state. Promise instances have three states.

-Asynchronous operation is not completed (pending) -Asynchronous operation is successful (fulfilled) -Asynchronous operation failed (rejected)

In the above three states, fulfilled and rejected together are called resolved (finalized).

There are only two ways to change these three states.

-From "incomplete" to "success" -From "incomplete" to "failed"

Once the state changes, it freezes, and there will be no new state changes. This is also the origin of the name Promise, which means "promise" in English. Once the promise is made, it cannot be changed. This also means that the state change of the Promise instance can only happen once.

Therefore, there are only two final results of Promise.

-The asynchronous operation is successful, the Promise instance returns a value, and the status becomes fulfilled. -The asynchronous operation fails, the Promise instance throws an error, and the status becomes rejected.

Promise Constructor

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 above code, the Promise constructor accepts a function as a parameter, and the two parameters of the function are resolve and reject. They are two functions, provided by the JavaScript engine, so you don't need to implement them yourself.

The role of the resolve function is to change the status of the Promise instance from "unfinished" to "successful" (that is, from pending to fulfilled). It is called when the asynchronous operation succeeds, and the asynchronous operation As a result, it is passed as a parameter. The role of the reject function is to change the status of the Promise instance from "unfinished" to "failed" (that is, from pending to rejected), call it when an asynchronous operation fails, and report the asynchronous operation The error is passed as a parameter.

Below 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 status of the instance will change to fulfilled.

Promise.prototype.then()

The then method of the Promise instance is used to add a callback function.

The then method can accept two callback functions. The first is the callback function when the asynchronous operation succeeds (becomes in the fulfilled state), and the second is the callback function when the asynchronous operation fails (becomes rejected). This parameter can be omitted). Once the state changes, call the corresponding callback function.

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: failed

In the above code, p1 and p2 are both Promise instances, and their then method binds two callback functions: the callback function console.log for success, and the callback function console.error for failure. (Can be omitted). The status of p1 becomes successful, and the status of p2 becomes failed. The corresponding callback function will receive the value returned by the asynchronous operation, and then output 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, there are four then after p1, which means that there are four callback functions in sequence. As long as the status of the previous step becomes fulfilled, the callback functions that follow immediately will be executed in turn.

The last then method, the callback function is console.log and console.error, there is an important difference in usage. console.log only displays the return value of step3, while console.error can display errors that occur in any of p1, step1, step2, and step3. For example, if the status of step1 becomes rejected, then both step2 and step3 will not be executed (because they are callback functions of resolved). Promise starts looking, and then the first callback function called rejected, in the above code is console.error. This means that the error reporting of the Promise object is transitive.

then() usage analysis

The usage of Promise is simply one sentence: use the then method to add a callback function. However, different writing methods have some subtle differences. Please see the following four writing methods. What is the difference between them?

// Writing method one
f1().then(function () {
  return f2();
});

// Writing method two
f1().then(function () {
  f2();
});

// Writing method three
f1().then(f2());

// Writing four
f1().then(f2);

In order to facilitate the explanation, the following four ways of writing all use the then method to connect to a callback function f3. The parameter of the f3 callback function written in method one is the result of the operation of the f2 function.

f1()
  .then(function () {
    return f2();
  })
  .then(f3);

The parameter of the f3 callback function in the second method is undefined.

f1()
  .then(function () {
    f2();
    return;
  })
  .then(f3);

The parameter of the f3 callback function in the third method is the running result of the function returned by the f2 function.

f1().then(f2()).then(f3);

There is only one difference between writing four and writing one, that is, f2 will receive the result returned by f1().

f1().then(f2).then(f3);

Example: Image loading

The following is the use of Promise to complete the loading of the picture.

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 monitoring properties, the onload property is called after the image is loaded successfully, and the onerror property is called when the loading fails.

The usage of the above preloadImage() function is 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 will return 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 the callback function becomes a standardized chain writing method, and the program flow can be seen clearly. It has a complete set of interfaces, which can implement many powerful functions, such as executing multiple asynchronous operations at the same time, and then executing a callback function after their states have changed; another example is to specify uniformly for errors thrown in multiple callback functions Processing methods and so on.

Moreover, Promise has another advantage that traditional writing does not have: once its state changes, it can be obtained whenever it is queried. This means that whenever you add a callback function to a Promise instance, the function will execute correctly. So, you don’t have to worry about whether you missed an event or signal. If it is the traditional way of writing, the callback function is executed by listening to the event. Once the event is missed, adding a callback function will not be executed.

The disadvantage of Promise is that it is more difficult to write than traditional writing, and reading the code is not easy to understand at a glance. You will only see a bunch of then, you have to sort out the logic in the callback function of then yourself.

Micro task

The callback function of Promise is an asynchronous task and will be executed after the synchronous task.

new Promise(function (resolve, reject) {
  resolve(1);
}).then(console.log);

console.log(2);
// 2
// 1

The above code will output 2 first, and then output 1. Because console.log(2) is a synchronous task, and the callback function of then is an asynchronous task and must be executed later than the synchronous task.

However, the callback function of Promise is not a normal asynchronous task, but a microtask. The difference between them is that normal tasks are added to the next round of event loop, and micro tasks are added to this round of event loop. This means that the execution time of the micro task must be 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 shows that the execution time of the callback function of then is earlier than setTimeout(fn, 0). Because then is executed in the current round of event loop, setTimeout(fn, 0) is executed at the beginning of the next round of event loop.