Promise principle analysis and source code implementation (following promise / A + specification)

Time:2020-10-27

–At the end of the paper, video tutorial + source code is attached

1. Constructor

new PromiseAn executor executor is required to be passed. The executor executes immediately (synchronously). The executor accepts two parameters, namely, resolve and reject.

Promise has three states:

-Pending: initial state, which is neither successful nor failed.
-Completed: successful status, which means the operation completed successfully.
-Rejected: failed state, which means the operation failed.
const PENDING = Symbol('PENDING')
const RESOLVED = Symbol('RESOLVED')
const REJECTED = Symbol('REJECTED')

//Promise constructor
function Promise (executor) {
  //The current status is pending by default
  this.status = PENDING
  //Save the callback function. Because then can be called multiple times, it is saved as an array
  this.onResolvedCallbacks = []
  this.onRejectedCallbacks = []
  //Success value
  this.value = undefined
  //Reasons for rejection
  this.reason = undefined
  //Resolve and reject are used to change the state,
  //The callback functions are called in turn according to the order in which the then method registers the callback functions
  // resolve is a function that is called after successful execution.
  const resolve = (value) => {
    //If the state is not pending, it indicates that the state has changed and cannot be changed again
    if (this.status === PENDING) {
      this.value = value
      this.status = RESOLVED
      this.onResolvedCallbacks.forEach(fn => fn())
    }
  }
  // reject is a function that is called after execution failure.
  const reject = (reason) => {
    if (this.status === PENDING) {
      this.reason = reason
      this.status = REJECTED
      this.onRejectedCallbacks.forEach(fn => fn())
    }
  }
  //Use try... Catch... To catch exceptions that may be thrown during code execution
  try {
    //The actuator executes immediately by default
    executor(resolve, reject)
  } catch(e) {
    //If an error occurs during the execution (including the exception thrown manually), it is equivalent to the execution failure
    reject(e)
  }
}

2. Then method implementation

Implementation of promisethenmethod,thenMethod has two optional parameters,onFulfilledandonRejectedAnd must return a promise object.
IfonFulfilledoronRejectedA promise is returned, which is executed automatically and its state is adopted. If successful, the result of the success is passed on to the next then in the outer layer.

Promise.prototype.then = function(onFulfilled, onRejected) {
  //Onfulfilled and onrejected are optional. Here, compatibility processing is required when the data is not transmitted
  //If onfulfilled is not a function, it builds a function and returns the result directly.
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
  //If onrejected is not a function, it builds a function and throws an exception directly.
  onRejected = typeof onRejected === 'function' ? onRejected : reason => {
    throw reason;
  }

  let promise2 = new Promise((resolve, reject) => {
    //When the status is resolved or rejected, it is mainly new project. When the executor calls resolve / reject, it is synchronous
    if (this.status === RESOLVED) {
      //Use the setTimeout (macro task) to ensure that the onfulfilled and onrejected methods are executed asynchronously, and that PROMISE2 is defined,
      //If setTimeout is not used, it will result in an error when resolvepromise (project2, x, resolve, reject) is executed.
      setTimeout(() => {
        //Try... Catch... Catch code errors or manually thrown exceptions, and handle them as execution failures. The error of asynchronous code cannot be caught by the outer try... Catch
        try {
          const x = onFulfilled(this.value)
          //X may be a promise or a normal value. The result returned by the onfulfilled or onrejected callback function in the then call of X needs to be passed to the next then callback function
          //The common method resolvepromise is used to deal with different situations and realize the transmission of x value.
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      }, 0)

      return
    }
    if (this.status === REJECTED) {
      setTimeout(() => {
        try {
          const x = onRejected(this.reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      }, 0)
    }
    //When the status is pending, mainly new promise, the call to resolve / reject in the executor is asynchronous
    if (this.status === PENDING) {
      //Because it is asynchronous, I don't know when the execution will be completed. Therefore, save the call (subscription) of the callback function first, and then execute (publish) after the state changes
      this.onResolvedCallbacks.push(() => {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }

        }, 0)
      })
      this.onRejectedCallbacks.push(() => {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      })
    }
  })
  return promise2
}

Some students may think thatthenIt is executed after the promise state changes (that is, after there is a return value)thenIt’s immediate, yesonFulfilledandonRejectedOnly after the state changes.

3. Implementation of promise resolution procedure

Promise resolution procedureIt is an abstract operation, which needs to enter a promise and a value, which we express as[[Resolve]](promise, x)

Here we define common methodsresolvePromiseTo implement this process.

resolvePromiseThe main functions are as follows:

  1. judgepromise2andxWhether to point to the same object, if sopromise2Execution failed with typeerror as the reason for the execution failure.

    For example:

    const p = new Promise((resolve, reject) => resolve(1))
    let promise2 = p.then(() => {
      // x
      return promise2
    })
  2. judgexIs it a promise object? If it is, it will get the status by calling resolve / reject and move to the next levelthenDelivery.
  3. IfxIs a normal object / value, thexNext as resultthenDelivery.

Here is the code implementation:

const resolvePromise = (promise2, x, resolve, reject) => {
  //If PROMISE2 and X point to the same object, PROMISE2 fails and typeerror is used as the reason for the failure
    if (promise2 === x) {
        return reject(new TypeError('Chaining cycle detected for promise #<promise>'))
    }
    if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
        //Prevent multiple calls from succeeding or failing
        let called;
        try {
      //First store a reference to X. then, and then test and call the reference to avoid multiple access to the X. then property
      //Prevent errors when taking x.then, for example,. Then is passed through Object.defineProperty  Defined, defined get () {} (getter) may have code errors or throw exceptions
      let then = x.then
      //I don't use X. then judgment because I'm afraid that I will make mistakes when I take. Then again. For example: through Object.defineProperty  The defined then may not report an error on the first call, and the value returned by the second call or multiple calls may be different
            if (typeof then === 'function') {
        //If then is a function, it is considered that x is a promise. If it is called with X as its this, then the state of X will be taken and returned with X's status
        //Two callback functions are passed as parameters. The first parameter is resolvepromise and the second is rejectproject
                then.call(x, y => { 
                    if (called) {
                        return
                    }
                    called = true
          //Y is the successful result of X calling then, and this result is adopted
          //Y may also be a promise, so make a recursive call until the result is a normal value
                    resolvePromise(promise2, y, resolve, reject)
                }, r => {
                    //R is to report an error or exception after calling x.then, no longer judge whether it is promise, and pass it directly
                    if (called) {
                        return
                    }
                    called = true
                    Reject (R); // the failure result is passed down
                });
            } else {
        //Normal object, passed directly to the next then
                resolve(x)
            }
        } catch (e) {
      //If a code error occurs or an exception is thrown manually, it will be handled when the execution fails and E is the cause of the failure
            if (called) {
                return
            }
            called = true
            reject(e)
        }
    } else {
    //Normal value, passed directly to the next then
        resolve(x)
    }
}

4. Implementation of deferred

Promise.defer = Promise.deferred = function () {
  let dfd = {}
  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}

deferredThe role of:

  1. usepromise-aplus-testTools need to use this method
  2. This method can reduce code nesting

    For example:

    const Promise = require('./pormise')
    const fs = require('fs')
    const readfile = url => {
      Return new promise ((resolve, reject) = > {// one level nesting
        fs.readFile (URL, 'UTF-8', (err, data) = > {// two level nesting
          if(err) reject(err)
          resolve(data)
        })
      })
    }
    readfile('./package.json').then(data => console.log(data))

    usedeferred

    const readfile = url => {
      let dfd = Promise.defer()
      //One level of nesting is reduced
      fs.readFile(url, 'utf-8', (err, data) => {
        if(err) dfd.reject(err)
        dfd.resolve(data)
      })
      return dfd.promise
    }

5. Testing

Promise aplus test

Installation:npm install -g promises-aplus-test

Test:promise-aplus-test promise.js

Using the GitHub source code provided in this article, you can directly run the following commands:

//Install dependent tools
npm install
//Run test instruction
npm run test

6. Other methods of promise

The core code of promise has been implemented above, but the native promise also provides some other methods.

  1. Promise.resolve()
  2. Promise.reject()
  3. Promise.all()
  4. Promise.race()
  5. Promise.prototype.catch()
  6. Promise.prototype.finally()

1)Promise.resolve()

Sometimes you need to convert an existing object into a promise object,Promise.resolve()Methods do that.

Promise.resolve()It is equivalent to the following.

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

Promise.resolveThe parameters of the method are divided into four cases.

  • The parameter is a promise,Promise.resolveDo not make any changes, return intact
  • The parameter is athenableObject,Promise.resolveMethod turns the object into a promise object and executes it immediatelythenableObject’sthenmethod.
  • Parameter does not havethenMethod, or is not an object at all,Promise.resolve Method returns a new promise object with a status ofresolved
  • Without any parameters, it returns aresolvedThe promise object of the state.
Promise.resolve = function (param) {
        if (param instanceof Promise) {
        return param;
    }
    return new Promise((resolve, reject) => {
        if (param && param.then && typeof param.then === 'function') {
            setTimeout(() => {
                param.then(resolve, reject);
            });
        } else {
            resolve(param);
        }
    });
}

2)Promise.reject()

Promise.reject(reason)Method also returns a new promise instance with a status ofrejected

Promise.reject()The parameters of the method will be left as they arerejectThe reason becomes a parameter of the subsequent method. This is related toPromise.resolveThe method is inconsistent.

Promise.reject = function (reason) {
    return new Promise((resolve, reject) => {
        reject(reason);
    });
}

3)Promise.all()

Promise.all() Method is used to wrap multiple promise instances into a new promise instance.

const p = Promise.all([p1, p2, p3]);

In the code above,Promise.all()Method takes an array as an argument,p1p2p3They are promise instances. If they are not, the following will be called firstPromise.resolveMethod, change the parameter to promise instance, and then process it further. Besides,Promise.all()The parameter of a method can be not an array, but it must have an iterator interface, and each member returned is a promise instance.

pThe state of thep1p2p3Decision, divided into two cases.

(1) Onlyp1p2p3The state offulfilledpWill become the state offulfilledAt this timep1p2p3The return value of the is an array passed to thepThe callback function of.

(2) As long asp1p2p3One of them wasrejectedpThe state ofrejectedAt this time, the first one isrejectThe return value of the instance of thepThe callback function of.

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    //Store the result. The parameter passed in by. All is an array, and the returned result is also data
    let result = []
    //Use a counter to record multiple asynchronous concurrent problems
    let index = 0
    if (promises.length === 0) {
      resolve(result)
    } else {
      //Processing return value
      function processValue(i, data) {
        result[i] = data
        //The number of counter records is equal to the length of the array passed in, indicating that all are considered to be completed and the result can be returned
        if (++index === promises.length) {
          resolve(result)
        }
      }
      for (let i = 0; i < promises.length; i++) {
        let current = promises[i]
        //Judge whether the current processing object is promise or normal value
        if (isPromise(current)) {
          //Take the execution result of the current processing object. If one of the execution fails, reject it directly
          current.then(data => {
            processValue(i, data)
          }, reject)
        } else {
          processValue(i, current)
        }
      }
    }
  })
}

4)Promise.race()

Promise.race()The method is also to wrap multiple promise instances into a new promise instance.

const p = Promise.race([p1, p2, p3]);

In the code above, as long asp1p2p3There’s one example that takes the lead in changing the state,pThe state of the system changes. The return value of the promise instance that first changed is passed to thepThe callback function of.

Promise.race()Method parameters andPromise.all()If it is not a promise instance, we will call the following firstPromise.resolve()Method, change the parameter to promise instance, and then process it further.

Promise.race = function (promises) {
    return new Promise((resolve, reject) => {
        if (promises.length === 0) {
            return;
        } else {
            for (let i = 0; i < promises.length; i++) {
                Promise.resolve(promises[i]).then((data) => {
                    resolve(data);
                    return;
                }, (err) => {
                    reject(err);
                    return;
                });
            }
        }
    });
}

5)Promise.prototype.catch()

Promise.prototype.catchThe method is.then(null, rejection)or.then(undefined, rejection)Alias for specifying the callback function when an error occurs.

Promise.prototype.catch = function (onRejected) {
    return this.then(null, onRejected);
}

6)Promise.prototype.finally()

finallyMethod is used to specify the action to be performed regardless of the final state of the promise object. This method is introduced by es2018.

Promise.prototype.finally = function (callback) {
    return this.then((value) => {
        return Promise.resolve(callback()).then(() => {
            return value;
        });
    }, (err) => {
        return Promise.resolve(callback()).then(() => {
            throw err;
        });
    });
}

Reference documents:

Promise / A + specification

Promise / A + specification

Ruan Yifeng – promise object – ecmascripts 6 Introduction

Promise – JavaScript | MDN

Get video tutorial + source code