In depth promise (1) — detailed explanation of promise implementation

Time:2021-3-8
if (typeof Promise === 'undefined') {
  return
} 

There are many libraries to implement promise / A + specification. Lie is a concise library to implement promise / A +, and has passed the promise / A + special test set. However, lie’s code is a bit convoluted. Based on Lie’s code, I modified it to make it easier to read and understand, and released the appointment module for your reference.
Promise / A + specification

There are many promise specifications, such as promise / A, promise / B, promise / D and promise / A + which is the upgraded version of promise / A. if you are interested, you can learn about it. Finally, promise / A + is adopted in ES6. Before explaining the promise implementation, of course, you need to understand the promise / A + specification. Promise / A + specification reference:

English version:https://promisesaplus.com/
Chinese version:http://malcolmyu.github.io/ma…
Note: without special explanation, the following promise all refer to promise instances.
Although the standard is not long, there are also many details. I’ll pick out a few key points and briefly explain them as follows:
Promise is essentially a state machine. Each project can only be in one of three states: pending, full or rejected. The state transition can only be pending > filled or pending > rejected. The state transition is irreversible.
The then method can be called multiple times by the same promise.
The then method must return a promise. The specification does not specify whether to return a new promise or reuse the old promise (return this). Most implementations return a new promise, and reusing the old promise may change the internal state, which is also contrary to the specification.
Value penetration. I’ll talk about it in detail.
Implement promise from scratch

We know that promise is a constructor, which needs to be called with new, and has the following APIs:

function Promise(resolver) {}

Promise.prototype.then = function() {}
Promise.prototype.catch = function() {}

Promise.resolve = function() {}
Promise.reject = function() {}
Promise.all = function() {}
Promise.race = function() {}

Next, let’s start to build a complete promise implementation step by step with the ultimate goal of appoint.

'use strict';

var immediate = require('immediate');

function INTERNAL() {}
function isFunction(func) {
  return typeof func === 'function';
}
function isObject(obj) {
  return typeof obj === 'object';
}
function isArray(arr) {
  return Object.prototype.toString.call(arr) === '[object Array]';
}

var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

module.exports = Promise;

function Promise(resolver) {
  if (!isFunction(resolver)) {
    throw new TypeError('resolver must be a function');
  }
  this.state = PENDING;
  this.value = void 0;
  this.queue = [];
  if (resolver !== INTERNAL) {
    safelyResolveThen(this, resolver);
  }
}

Immediate is a library that will execute synchronously to asynchronously. Internal is an empty function, similar to NOOP in some code bases. Three auxiliary functions are defined: isfunction, isobject and isarray. Three states are defined: pending, fullled and rejected. Safe resolve then. There are three variables in promise
State: the state of the current promise. The initial value is pending. The state change can only be pending > full LED or pending > rejected.
Value: the return value is stored when the state is full, and the error is stored when the state is rejected.
Queue: the callback queue inside promise. What is this? Why an array?
Basic principles of promise implementation

Let’s take a look at the code:

var Promise = require('appoint')
var promise = new Promise((resolve) => {
  setTimeout(() => {
    resolve('haha')
  }, 1000)
})
var a = promise.then(function onSuccess() {})
var b = promise.catch(function onError() {})
console.log(require('util').inspect(promise, { depth: 10 }))
console.log(promise.queue[0].promise === a)
console.log(promise.queue[1].promise === b)

Print out:

Promise {
  state: 0,
  value: undefined,
  queue:
   [ QueueItem {
       promise: Promise { state: 0, value: undefined, queue: [] },
       callFulfilled: [Function],
       callRejected: [Function] },
     QueueItem {
       promise: Promise { state: 0, value: undefined, queue: [] },
       callFulfilled: [Function],
       callRejected: [Function] } ] }
true
true

As you can see, there are two objects in the queue array. Because the specification stipulates that the then method can be called multiple times by the same promise. In the above example, when. Then and. Catch are called, the project is not resolved, so the new project (A and b) generated by. Then and. Catch, the correct callback (onsuccess) and the wrong callback (onerror) are packaged as callcompleted to generate a queueitem instance and push it into the queue array. Therefore, the above two examples are not resolved console.log Printing true。 When the state of the project changes, it traverses the internal queue array, uniformly executes the successful (fullled > – callfulfed) or failed (rejected > – callrejected) callbacks (pass in the value of the project), and sets the state and value of a and B respectively for the generated results. This is the basic principle of the implementation of the project.
Let’s take another example

var Promise = require('appoint')
var promise = new Promise((resolve) => {
  setTimeout(() => {
    resolve('haha')
  }, 1000)
})
promise
  .then(() => {})
  .then(() => {})
  .then(() => {})
console.log(require('util').inspect(promise, { depth: 10 }))

Print out:

Promise {
  state: 0,
  value: undefined,
  queue:
   [ QueueItem {
       promise:
        Promise {
          state: 0,
          value: undefined,
          queue:
           [ QueueItem {
               promise:
                Promise {
                  state: 0,
                  value: undefined,
                  queue:
                   [ QueueItem {
                       promise: Promise { state: 0, value: undefined, queue: [] },
                       callFulfilled: [Function],
                       callRejected: [Function] } ] },
               callFulfilled: [Function],
               callRejected: [Function] } ] },
       callFulfilled: [Function],
       callRejected: [Function] } ] }

Then is called three times, and each then puts its generated promise into the promise queue that calls it, forming a three-tier call relationship. When the outermost promise state changes, traverse its queue array to call the corresponding callback, set the state and value of the child promise and traverse its queue array to call the corresponding callback, then set the state and value of the child promise and traverse its queue array to call the corresponding callback, and so on.
safelyResolveThen

function safelyResolveThen(self, then) {
  var called = false;
  try {
    then(function (value) {
      if (called) {
        return;
      }
      called = true;
      doResolve(self, value);
    }, function (error) {
      if (called) {
        return;
      }
      called = true;
      doReject(self, error);
    });
  } catch (error) {
    if (called) {
      return;
    }
    called = true;
    doReject(self, error);
  }
}

Safelyresolve then, as the name suggests, is used to “execute then function safely”. The then function here refers to “the first parameter is the resolve function, and the second parameter is the function of reject function”. There are two cases as follows:
1. The parameters of the constructor, i.e. resolver here:

new Promise(function resolver(resolve, reject) {
  setTimeout(() => {
    resolve('haha')
  }, 1000)
})

2. Then of promise:

promise.then(resolve, reject)

Safelyresolvethen has three functions
1. Try… Catch catch the exception thrown, such as:

new Promise(function resolver(resolve, reject) {
throw new Error(‘Oops’)
})

2. The called control resolves or rejects only once, and multiple calls have no effect. Namely:

var Promise = require('appoint')
var promise = new Promise(function resolver(resolve, reject) {
  setTimeout(() => {
    resolve('haha')
  }, 1000)
  reject('error')
})
promise.then(console.log)
promise.catch(console.error)

Print error, no more haha.
3. If there are no errors, do resolve will be executed, and if there are errors, do reject will be executed.

Do resolve and do reject

function doResolve(self, value) {
  try {
    var then = getThen(value);
    if (then) {
      safelyResolveThen(self, then);
    } else {
      self.state = FULFILLED;
      self.value = value;
      self.queue.forEach(function (queueItem) {
        queueItem.callFulfilled(value);
      });
    }
    return self;
  } catch (error) {
    return doReject(self, error);
  }
}

function doReject(self, error) {
  self.state = REJECTED;
  self.value = error;
  self.queue.forEach(function (queueItem) {
    queueItem.callRejected(error);
  });
  return self;
}

Do reject is to set the state of the project as rejected, value as error, and call rejected. As mentioned in the previous notice sub project: “I have a problem here.” then the sub project sets its own state and value according to the incoming error. Doresolve is used in combination with safelyresolvethen to continuously unpack promise until the return value is a non promise object, set the promise state and value, and then notify the child promise: “I have a value here.” then the child promise sets its own state and value according to the incoming value.

Here is an auxiliary function getthen:

function getThen(obj) {
  var then = obj && obj.then;
  if (obj && (isObject(obj) || isFunction(obj)) && isFunction(then)) {
    return function appyThen() {
      then.apply(obj, arguments);
    };
  }
}

The specification stipulates that if then is a function, X (here obj) will be called as this of the function.
Promise.prototype.then And Promise.prototype.catch

Promise.prototype.then = function (onFulfilled, onRejected) {
  if (!isFunction(onFulfilled) && this.state === FULFILLED ||
    !isFunction(onRejected) && this.state === REJECTED) {
    return this;
  }
  var promise = new this.constructor(INTERNAL);
  if (this.state !== PENDING) {
    var resolver = this.state === FULFILLED ? onFulfilled : onRejected;
    unwrap(promise, resolver, this.value);
  } else {
    this.queue.push(new QueueItem(promise, onFulfilled, onRejected));
  }
  return promise;
};

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

Return this in the above code implements value penetration, which will be discussed later. As you can see, a new promise is generated in the then method and returned, which meets the specification requirements. If the state of promise changes, unwrap will be called. Otherwise, the generated promise will be added to the callback queue of the current promise. Previously, we explained how to consume the queue. There are three points to explain:
1. The promise constructor passes in an internal function, that is, an empty function, because the newly generated promise can be regarded as an internal promise. It needs to generate its own state and value according to the state and value of the external promise. It does not need to pass in a callback function, but the external promise needs to pass in a callback function to determine its state and value. Therefore, the constructor of promise made a judgment to distinguish external calls from internal calls

if (resolver !== INTERNAL) {
  safelyResolveThen(this, resolver);
}

2. The unwrap code is as follows:

function unwrap(promise, func, value) {
  immediate(function () {
    var returnValue;
    try {
      returnValue = func(value);
    } catch (error) {
      return doReject(promise, error);
    }
    if (returnValue === promise) {
      doReject(promise, new TypeError('Cannot resolve promise with itself'));
    } else {
      doResolve(promise, returnValue);
    }
  });
}

It can also be understood from the name that it is used to unpack (that is, to execute a function). The first parameter is the child promise, the second parameter is the callback (onfulfilled / onrejected) of the then of the parent promise, and the third parameter is the value (normal value / error) of the parent promise. There are three points to explain:
1. Use immediate to synchronize the code mutation step. For example:

var Promise = require('appoint')
var promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('haha')
  }, 1000)
})
promise.then(() => {
  promise.then(() => {
    console.log('1')
  })
  console.log('2')
})

Print 2 1, remove immediate and print 1 2.
2. Try… Catch is used to catch the exception thrown in then / catch and call dorject, such as:

promise.then(() => {
  throw new Error('haha')
})
promise.catch(() => {
  throw new Error('haha')
})

3. The returned value cannot be promise itself, otherwise it will cause a dead loop, such as [email protected] Run under the following conditions:

var promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('haha')
  }, 1000)
})
var a = promise.then(() => {
  return a
})

a.catch(console.log)// [TypeError: Chaining cycle detected for promise #<Promise>]

The code of queueitem is as follows:

function QueueItem(promise, onFulfilled, onRejected) {
  this.promise = promise;
  this.callFulfilled = function (value) {
    doResolve(this.promise, value);
  };
  this.callRejected = function (error) {
    doReject(this.promise, error);
  };
  if (isFunction(onFulfilled)) {
    this.callFulfilled = function (value) {
      unwrap(this.promise, onFulfilled, value);
    };
  }
  if (isFunction(onRejected)) {
    this.callRejected = function (error) {
      unwrap(this.promise, onRejected, error);
    };
  }
}
promi

Se is the new project generated by then (hereinafter referred to as “sub project”), onfulfilled and onrejected are onfulfilled and onrejected in then parameters. It can be seen from the above code: for example, when the promise state changes to full, the then function registered previously, calls unwrap with callfuled to unpack, and finally obtains the state and value of the sub promise. The catch function registered previously, calls doresolve directly with callfuled to set the state and value of the sub promise in the queue. When the promise state changes to rejected, it is similar.
be careful: promise.catch (on rejected) is promise.then The syntax of (null, onrejected).
So far, the core implementation of promise has been completed.
Value penetration

Promise.prototype.then = function (onFulfilled, onRejected) {
  if (!isFunction(onFulfilled) && this.state === FULFILLED ||
    !isFunction(onRejected) && this.state === REJECTED) {
    return this;
  }
  ...
};

The problem of value penetration is mentioned above

var Promise = require('appoint')
var promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('haha')
  }, 1000)
})
promise
  .then('hehe')
  .then(console.log)

Finally print haha instead of hehe.
Only one case of value penetration is realized through return this. There are two cases of value penetration
1. When promise is already fullled / rejected, the value penetration realized by return this is as follows:

var Promise = require('appoint')
var promise = new Promise(function (resolve) {
  setTimeout(() => {
    resolve('haha')
  }, 1000)
})
promise.then(() => {
  promise.then().then((res) => {// ①
    console.log(res)// haha
  })
  promise.catch().then((res) => {// ②
    console.log(res)// haha
  })
  console.log(promise.then() === promise.catch())// true
  console.log(promise.then(1) === promise.catch({ name: 'nswbmw' }))// true
})

The promise at (1) and (2) of the above code is already full, so return this is executed. Note: the native promise implementation is not implemented in this way, so two false will be printed.
2. When a project is pending, it is added to the parent project’s queue by generating a new project. When the parent project has a value, it calls callfulfilled – > doresolve or callrejected – > doreeject (because the parameter passed in then / catch is not a function) to set the state and value of the child project to the state and value of the parent project. For example:

var Promise = require('appoint')
var promise = new Promise((resolve) => {
  setTimeout(() => {
    resolve('haha')
  }, 1000)
})
var a = promise.then()
a.then((res) => {
  console.log(res)// haha
})
var b = promise.catch()
b.then((res) => {
  console.log(res)// haha
})
console.log(a === b)// false

Promise.resolve And Promise.reject

Promise.resolve = resolve;
function resolve(value) {
  if (value instanceof this) {
    return value;
  }
  return doResolve(new this(INTERNAL), value);
}

Promise.reject = reject;
function reject(reason) {
  var promise = new this(INTERNAL);
  return doReject(promise, reason);
}

When Promise.resolve When the parameter is a promise, the value is returned directly.

Promise.all

Promise.all = all;
function all(iterable) {
  var self = this;
  if (!isArray(iterable)) {
    return this.reject(new TypeError('must be an array'));
  }

  var len = iterable.length;
  var called = false;
  if (!len) {
    return this.resolve([]);
  }

  var values = new Array(len);
  var resolved = 0;
  var i = -1;
  var promise = new this(INTERNAL);

  while (++i < len) {
    allResolver(iterable[i], i);
  }
  return promise;
  function allResolver(value, i) {
    self.resolve(value).then(resolveFromAll, function (error) {
      if (!called) {
        called = true;
        doReject(promise, error);
      }
    });
    function resolveFromAll(outValue) {
      values[i] = outValue;
      if (++resolved === len && !called) {
        called = true;
        doResolve(promise, values);
      }
    }
  }
}

Promise.all It is used to execute multiple promise / values in parallel. When all promise / values are executed or one of them has an error, it returns. It can be seen that:
1、 Promise.all A new promise return is generated internally.
2. Called is used to control that even if there are multiple project reviews, only the first one will take effect.
3. Values are used to store the results.
4. When the last promise gets the result, use doresolve (promise, values) to set the state of promise as fullled and value as the result array values.
Promise.race

Promise.race = race;
function race(iterable) {
  var self = this;
  if (!isArray(iterable)) {
    return this.reject(new TypeError('must be an array'));
  }

  var len = iterable.length;
  var called = false;
  if (!len) {
    return this.resolve([]);
  }

  var i = -1;
  var promise = new this(INTERNAL);

  while (++i < len) {
    resolver(iterable[i]);
  }
  return promise;
  function resolver(value) {
    self.resolve(value).then(function (response) {
      if (!called) {
        called = true;
        doResolve(promise, response);
      }
    }, function (error) {
      if (!called) {
        called = true;
        doReject(promise, error);
      }
    });
  }
}

Promise.race Accepts an array and returns when there is a resolve or reject in the array. With Promise.all The code is similar, except that the called control is used here. As long as there is any project onfulfilled / onrejected, the status and value of the project will be set immediately.
So far, the implementation of promise is all explained.