The profile

  • The best way to learn a technology is to understand how it was created and what problems it solves.
  • The following will be introduced from the following aspectsPromise:
    • Problems with asynchronous programming: discontinuous code logic;
    • Callback hell: too many nested callback functions;
    • Promise: Kills nested calls;
    • Promise: Error handling for merging multiple tasks;
    • PromiseRelationship with microtask;
  • To be clear, promises address asynchronous coding styles, not some other issue;

The problem with asynchronous programming: Discontinuous code logic

  • Suppose there is a request, useXMLHttpRequestTo implement, the code is as follows:
    // Execution status
    function onResolve(response) { console.log(response); }
    function onReject(error) { console.log(error); }
    
    let xhr = new XMLHttpRequest();
    xhr.ontimeout = function(e) { onReject(e); };
    xhr.onerror = function(e) { onReject(e); };
    xhr.onreadystatechange = function () { onResolve(xhr.response); };
    
    // Set the request type, request URL, and whether to synchronize information
    let URL = 'https://localhost:8080/getList';
    xhr.open('Get', URL, true);
    
    // Set parameters
    xhr.timeout = 3000; // Set the timeout period for XHR requests
    xhr.responseType = "text"; // Format the data to be returned in the response
    xhr.setRequestHeader("X_TEST"."time.geekbang");
    
    // Make a request
    xhr.send();
    Copy the code
    • So there are five callbacks in this short piece of code, so many callbacks will lead to the logic of the code is not coherent, not linear, very intuitive, this is how asynchronous callbacks affect our coding;

Callback hell: Too many nested callback functions

  • It is also possible to wrap the above asynchronous code into a function, which can be called by passing in the corresponding request parameters, callback function, so that the processing flow becomes linear, like the following code:

    // Request, request information, request header, delay value, return type, etc
    // resolve, execute successfully, callback this function
    // reject fails to execute, callback this function
    function XFetch(request, resolve, reject) {
        let xhr = new XMLHttpRequest();
        xhr.ontimeout = function (e) { reject(e); };
        xhr.onerror = function (e) { reject(e); };
        xhr.onreadystatechange = function () {
            if (xhr.status = 200)
                resolve(xhr.response);
        };
    
        xhr.open(request.method, request.url, request.sync);
        xhr.timeout = request.timeout;
        xhr.responseType = request.responseType;
        // Add additional request information
        // ...
    
        xhr.send();
    }
    
    // Call wrapped asynchronous code
    XFetch(
        { method: 'GET'.url: 'https://localhost:8080/getList'.sync: true },
        function resolve(data) {
            console.log(data);
        }, 
        function reject(e) {
            console.log(e); });Copy the code
  • Encapsulating asynchronous code is fine for some simple logic, but if the logic is more complex, it can create a callback hell by nesting a lot of callback functions, like this:

    XFetch(
        { method: 'GET'.url: 'https://localhost:8080/getList'.sync: true },
        function resolve(response) {
          console.log(response);
          XFetch(
              { method: 'GET'.url: 'https://localhost:8080/getList1'.sync: true },
              function resolve(response) {
                  console.log(response);
                  XFetch(
                      { method: 'GET'.url: 'https://localhost:8080/getList2'.sync: true },
                      function resolve(response) {
                          console.log(response);
                      },
                      function reject(e) {
                          console.log(e); })},function reject(e) {
                  console.log(e); })},function reject(e) {
          console.log(e); });Copy the code
  • The above code looks messy for two reasons:

    • Nested calls: The next task relies on the result of the previous task’s request and executes the new business logic inside the callback function of the previous task. As the nesting level increases, the code becomes very unreadable.
    • Task uncertainty: Executing each task has two possible outcomes (success or failure), so an additional error handling for each task significantly increases the level of code clutter;
  • After analyzing the reasons, these two problems need to be solved:

    • Eliminate nested calls;
    • Error handling for merging multiple tasks;
  • ES6 introduced Promise to solve these two problems, which are described below.

Promise: Kills nested calls

Promise solves the nested callback problem primarily in two steps:

  • Promise implements delayed binding of callback functions:

    // Create the Promise object promise1 and execute the business logic in the executor function
    function executor(resolve, reject){
        resolve(100);
    }
    let promise1 = new Promise(executor);
    
    // promise1 deferred binding callback onResolve
    function onResolve(value){
        console.log(value);
    }
    promise1.then(onResolve);
    Copy the code
    • As shown above:
      • The delayed binding of the callback function is created first in the codePromiseobjectpromise1;
      • throughPromiseConstructor ofexecutorTo execute the business logic;
      • To create a goodPromiseobjectpromise1After that, use it againpromise1.then()To set the callback function;
    • In short, Promise implements the delayed binding of callback functions, separating the original callback writing method, and executing the callback function in the way of chain call after the asynchronous operation is completed.
  • Promise penetrates the return value of the callback function onResolve to the outermost layer:

    // Create the Promise object promise1 and execute the business logic in the executor function
    function executor(resolve, reject) {
        resolve(100);
    }
    const promise1 = new Promise(executor);
    
    // promise1 deferred binding callback onResolve
    function onResolve(value) {
        console.log(value);
    
        function executor2(resolve, reject) {
            resolve(value + 1);
        }
        return new Promise(executor2);
    }
    
    // promise2 penetrates to the outermost layer for the inner return value
    const promise2 = promise1.then(onResolve);
    
    promise2.then((value) = > {
        console.log(value);
    });
    Copy the code
    • As shown above, you can get rid of the nested loop by returning the Promise object created inside onResolve to the outermost layer;

Promise: Error handling for merging multiple tasks

function executor(resolve, reject) {
    let rand = Math.random();
    console.log(1);
    console.log(rand);
    if (rand > 0.5)
        resolve();
    else
        reject();
}
var p0 = new Promise(executor);

var p1 = p0.then((value) = > {
    console.log("succeed-1");
    return new Promise(executor);
});

var p3 = p1.then((value) = > {
    console.log("succeed-2");
    return new Promise(executor);
});

var p4 = p3.then((value) = > {
    console.log("succeed-3");
    return new Promise(executor);
});

p4.catch((error) = > {
    console.log("error");
})
console.log(2);
Copy the code
  • Above code, chain calls fourPromiseObject:P0 ~ p4.Any object that throws an exception can pass through the last objectp4.catchTo catch exceptions;
  • In this way you can put allPromiseObject errors are consolidated into a single function, thus eliminating the need for each task to handle exceptions separately;
  • The last object can be used to catch all exceptions because errors on the Promise object are “bubbling” and passed backwards until they are processed by the onReject function or caught by a catch statement.
  • With this “bubbling” feature, there is no need for eachPromiseAn exception is caught separately in the

PromiseRelationship to microtasks

function executor(resolve, reject) {
    resolve(100)}let demo = new Promise(executor)

function onResolve(value){
    console.log(value)
}
demo.then(onResolve)
Copy the code
  • Code like this:

    • Executed firstnew PromiseWhen,PromiseThe constructor is executed;
    • Next,PromiseThe constructor of thePromiseThe parameters of theexecutorFunctions;
    • Then, inexecutorTo perform theresolve.
    • performresolveFunction, will firedemo.thenSet the callback functiononResolve;
      • So presumably,resolveThe function is called internally throughdemo.thenSet up theonResolveFunctions;
  • Note: Since Promise uses the callback function deferred binding technology, the callback function is not bound when the resolve function is executed, so the callback function can only be delayed.

  • For ease of understanding, a simple Promise object is implemented:

    function Bromise(executor) {
        var onResolve_ = null;
        var onReject_ = null;
         // Implement resolve and then
        this.then = function (onResolve, onReject) {
            onResolve_ = onResolve;
        };
        function resolve(value) {
              //setTimeout(()=>{
                onResolve_(value);
               / /}, 0)
        }
        executor(resolve, null);
    }
    Copy the code
  • Call the Bromise object defined above:

    function executor(resolve, reject) {
        resolve(100);
    }
    / / call Bromsie
    let demo = new Bromise(executor);
    
    function onResolve(value) {
        console.log(value);
    }
    demo.then(onResolve);
    Copy the code
    • An error is reported because we are executingexecutorFunction, it hasn’t passed yetdemo.then()Set the callback function,BromiseIn theonResolve_It’s still empty, so it’s an error;
  • This is where you need to modify the resolve method in Bromise to defer calling onResolve_ :

    • To make theresolveIn theonResolve_ Function deferred execution, can be inresolveFunction inside add a timer, let its delay executiononResolve_Function, code as follows:
      function Bromise(executor) {
          var onResolve_ = null;
          var onReject_ = null;
          // Implement resolve and then
          this.then = function (onResolve, onReject) {
              onResolve_ = onResolve;
          };
          function resolve(value) {
                // Use the setTimeout timer to delay the execution of the onResolve_ function
                setTimeout(() = > {
                  onResolve_(value);
                }, 0);
          }
          executor(resolve, null);
      }
      Copy the code
    • It uses a timer to delayonResolveBut the efficiency of using timer is not too high;
  • Therefore, Promise transforms this timer into a microtask, which not only allows onResolve_ to be called delayed, but also improves the execution efficiency of the code. This is why Promise uses microtasks.

Geeky time: Say goodbye to the callback function