Illustration of Promise implementation principle -- Promise static method implementation

Posted by Dragonfly on Mon, 01 Jun 2020 05:18:43 +0200

This article starts with WeChat official account of vivo Internet technology.
Link: https://mp.weixin.qq.com/s/Lp_5BXdpm7G29Z7zT_S-bQ
By: Morrain

Provides the Promise object. For more information about Promise, please refer to teacher Ruan Yifeng's Promise object for getting started with ES6.

When learning Promise, many students know it but don't know why. They can't understand its usage. This series of articles gradually realize Promise from shallow to deep, and demonstrate with flow chart, examples and animation to achieve the purpose of deep understanding of Promise usage.

This series consists of the following chapters:

  1. Schematic principle of Promise (1) - basic implementation

  2. Illustration of Promise implementation principle (2) -- Promise chain call

  3. Illustration of the principle of Promise (3) -- the realization of Promise prototype method

  4. Illustration of the principle of Promise (4) -- the realization of Promise static method

1, Foreword

In the previous section, we implemented the prototype method of Promise. Including adding exception status, catch and finally. Up to now, Promise has been implemented as follows:

class Promise {
  callbacks = [];
  state = 'pending';//Increase status
  value = null;//Save results
  constructor(fn) {
    fn(this._resolve.bind(this), this._reject.bind(this));
  }
  then(onFulfilled, onRejected) {
    return new Promise((resolve, reject) => {
      this._handle({
        onFulfilled: onFulfilled || null,
        onRejected: onRejected || null,
        resolve: resolve,
        reject: reject
      });
    });
  }
  catch(onError) {
    return this.then(null, onError);
  }
  finally(onDone) {
    if (typeof onDone !== 'function') return this.then();
 
    let Promise = this.constructor;
    return this.then(
      value => Promise.resolve(onDone()).then(() => value),
      reason => Promise.resolve(onDone()).then(() => { throw reason })
    );
  }
  _handle(callback) {
    if (this.state === 'pending') {
      this.callbacks.push(callback);
      return;
    }
 
    let cb = this.state === 'fulfilled' ? callback.onFulfilled : callback.onRejected;
 
    if (!cb) {//If nothing is passed in then
      cb = this.state === 'fulfilled' ? callback.resolve : callback.reject;
      cb(this.value);
      return;
    }
 
    let ret;
 
    try {
      ret = cb(this.value);
      cb = this.state === 'fulfilled' ? callback.resolve : callback.reject;
    } catch (error) {
      ret = error;
      cb = callback.reject
    } finally {
      cb(ret);
    }
 
  }
  _resolve(value) {
 
    if (value && (typeof value === 'object' || typeof value === 'function')) {
      var then = value.then;
      if (typeof then === 'function') {
        then.call(value, this._resolve.bind(this), this._reject.bind(this));
        return;
      }
    }
 
    this.state = 'fulfilled';//Change state
    this.value = value;//Save results
    this.callbacks.forEach(callback => this._handle(callback));
  }
  _reject(error) {
    this.state = 'rejected';
    this.value = error;
    this.callbacks.forEach(callback => this._handle(callback));
  }
}

Next, I will introduce the implementation of static methods in promise, such as Promise.resolve , Promise.reject , Promise.all And Promise.race . The implementation of other static methods is similar.

2, Static method

1,Promise.resolve && Promise.reject

In addition to the prototype methods for promise instances mentioned earlier, promise also provides Promise.resolve and Promise.reject method. Used to wrap a non promise instance as a promise instance. For example:

Promise.resolve('foo')
// Equivalent to
new Promise(resolve => resolve('foo'))

Promise.resolve If the parameters of are different, the corresponding processing will be different Promise.resolve The parameter of is an instance of promise Promise.resolve This promise instance will be returned directly without any changes. If it is a basic data type, such as the string in the above example, Promise.resolve A new promise instance will be created and returned. In this way, when we don't know whether the object we get is a promise instance or not, in order to ensure uniform behavior, Promise.resolve It becomes very useful. Take an example:

const Id2NameMap = {};
const getNameById = function (id) {
 
  if (Id2NameMap[id]) return Id2NameMap[id];
 
  return new Promise(resolve => {
    mockGetNameById(id, function (name) {
      Id2NameMap[id] = name;
      resolve(name);
    })
  });
}
getNameById(id).then(name => {
  console.log(name);
});

In the above scenario, we will often encounter, in order to reduce the request, we often cache the data. After we get the name corresponding to the ID, we save it in the Id2NameMap object. Next time we request the name corresponding to the ID through the ID, we first check whether it is in the Id2NameMap. If it is, we will directly return the corresponding name. If it is not, we will initiate an asynchronous request and put it in the Id2NameMap Middle to middle.

In fact, the above code is problematic. If the value in Id2NameMap is hit, the result returned by getNameById is name, not promise instance. At this time, getNameById(id).then will report an error. If we don't know whether the returned instance is a promise instance, we can use Promise.resolve Packaging:

Promise.resolve(getNameById(id)).then(name => {
  console.log(name);
});

In this way, no matter what is returned by getNameById(id), the logic is OK. Look at the Demo below:

demo-Promise.resolve Source code of

In implementation Promise.resolve Before that, let's look at its parameters:

(1) Parameter is a Promise instance

If the parameter is a promise instance, then Promise.resolve This instance will be returned unchanged without any modification.

(2) Parameter is a tenable object

The possible object refers to the object with the then method, such as the following object.

let thenable = {
  then: function(onFulfilled) {
    onFulfilled(42);
  }
};

Promise.resolve Method converts the object to a promise object and executes the then method of the tenable object immediately.

let thenable = {
  then: function(onFulfilled) {
    onFulfilled(42);
  }
};
 
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  // 42
});

In the above code, after the then method of thenable object is executed, the state of object p1 changes to resolved, so as to immediately execute the callback function specified by the last then method, and output 42.

(3) Parameter is not an object with a then method, or is not an object at all

If the parameter is an original value or an object that does not have the then method, the Promise.resolve Method returns a new promise object with the status resolved.

(4) Without task parameters

Promise.resolve Method allows the call to return a resolved promise object without parameters.

static resolve(value) {
  if (value && value instanceof Promise) {
    return value;
  } else if (value && typeof value === 'object' && typeof value.then === 'function') {
    let then = value.then;
    return new Promise(resolve => {
      then(resolve);
    });
 
 
  } else if (value) {
    return new Promise(resolve => resolve(value));
  } else {
    return new Promise(resolve => resolve());
  }
}

Promise.reject And Promise.resolve Similar, but different Promise.reject Always return a project instance of rejected with a status, and Promise.resolve If the parameter of is a promise instance, it returns the promise instance corresponding to the parameter, so the status is not necessarily.

Promise.reject Implementation source code of

2,Promise.all && Promise.race

Promise.all Receive an array of promise instances, and return the array of corresponding results according to the order of promise instances after all the instances of promise are fulfilled.

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('p1'), 1000)
})
 
 
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('p2'), 5000)
})
 
Promise.all([p1, p2]).then(rets => {
   console.log(rets) // ['p1','p2']
})

Promise.all The implementation is as follows:

static all(promises) {
  return new Promise((resolve, reject) => {
    let fulfilledCount = 0
    const itemNum = promises.length
    const rets = Array.from({ length: itemNum })
    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(result => {
        fulfilledCount++;
        rets[index] = result;
        if (fulfilledCount === itemNum) {
          resolve(rets);
        }
      }, reason => reject(reason));
    })
  })
}

Promise.all Implementation source code of

Promise.race Also receives an array of promise instances, and Promise.all The difference is that the result returned is the first one of these promise instances to be fulfilled.

const p1 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('p1'), 1000)
})
 
 
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => resolve('p2'), 5000)
})
 
Promise.race([p1, p2]).then(ret => {
   console.log(ret) // 'p1'
})

Promise.race The implementation is as follows:

static race(promises) {
  return new Promise(function (resolve, reject) {
    for (let i = 0; i < promises.length; i++) {
      Promise.resolve(promises[i]).then(function (value) {
        return resolve(value)
      }, function (reason) {
        return reject(reason)
      })
    }
  })
}

Promise.race Implementation source code of

3, Summary

At the beginning of looking at the Promise source code, you can't understand the operation mechanism of the then and resolve functions very well, but if you calm down and deduce according to the logic when executing Promise, it's not difficult to understand. Here we must pay attention to the following points: the then function in Promise only registers the subsequent code to be executed, and the real execution is performed in the resolve method. After clearing this layer, it will save a lot of effort to analyze the source code.

Now review the implementation process of Promise, which mainly uses the observer pattern in the design pattern:

  1. Through Promise.prototype.then And Promise.prototype.catch Method registers the observer method with the observer promise object and returns a new promise object so that it can be called in chain.

  2. The observed manages the state transition of internal pending, fulfilled and rejected, and actively triggers the state transition and notifies the observer through the resolve and reject methods passed in the constructor.

This series of pictures and texts explains the idea of Promise, and the content of implementation does not fully meet all the requirements of Promise/A + specification.

4, References

  1. Promises/A + specification

  2. In depth Promise(1) -- a detailed explanation of Promise implementation

  3. 30 minutes, let you fully understand Promise principle

More content, please pay attention to vivo Internet technology WeChat official account.

Note: please contact Labs2020 for reprint.

Topics: Front-end