A simple introduction

What Promise is, I believe it goes without saying that people who have written JS have been exposed to it at some point. When I first used Promise, I felt strange writing this way, but as I got familiar with it, I found everything was so harmonious.

Promise, as I understand it, addresses the nesting of asynchronous callbacks and represents a consequence of asynchronous operations. It is a state machine, a callback wrapper in terms of implementation (if you disagree with your own understanding, please discuss it).

After really looking at the Promise/A+ specification and other people’s implementation code, I found that the principle and implementation of Promise is not very difficult, so I have this article, which aims to learn from myself and may also help some people. This is the complete code and github address of the final Promise implementation. I will be updating frequently to learn in-depth articles on modern JS frameworks. Welcome star.

Promise specification

Promise has been available in various versions of “Promise” since before ES6, but when it comes to using them all together, it can be disastrous without a unified implementation standard. So there’s the specification, and Promise/A+ is Promise’s current specification. Specifications are intended to help implementers specify goals to achieve, not how to achieve them. Once you’ve written your Promise, to determine if it meets the Promise/A+ specification, you can use Promises -Tests.

Principles and Implementation

Let’s start by thinking about what the implementation of promises should look like. Promise encapsulates and hides our asynchronous callbacks, allowing us to chain callbacks and error handling into then functions, so the normal idea would be to store the callbacks in THEN and call them by Promise when the asynchro is done.

The first is the Promise constructor

function Promise (executor) {
  // To prevent this from being corrupted for aspect subsequent function calls
  var self = this;
  this.status = 'pending';
  this.value = null;
  this.reason = null;
  // Resolve callback queue
  this.resolveCbs = [];
  // reject callback queue
  this.rejectCbs = [];

  // Change the state to resolve, save the value passed in, and invoke the appropriate callback queue
  function resolve(value) {
    if (self.status === 'pending') {
      // Since the promise needs to be executed asynchronously, setTimeout is used to delay execution
      setTimeout(function() {
        self.status = 'fulfilled';
        self.value = value;
        self.resolveCbs.forEach(cb= >cb(value)); }); }}/// Resolve is the same as resolve, but it saves the cause and changes the state to Rejected
  function reject(reason) {
    if (self.status = 'pending') {
      setTimeout(function() {
        self.status = 'rejected';
        self.reason = reason;
        self.rejectCbs.forEach(cb= > cb(reason));
      });
    }
  }

  executor(resolve, reject);
}
Copy the code

Promise has three states, which are pending, depressing and Rejected. Its state can be changed from Pending to depressing and from Pending to Rejected. Once the state is determined, it cannot be changed.

With the constructor, let’s implement the key function in Promise, the then function. To implement the chained call, we need to return a Promise object, but we can’t return ourselves because the state of the Promise cannot be changed, and we need to return a new Promise object. The basic explanation is contained in the comments

Promise.prototype.then = function (onResolved, onRejected) {
  var self = this;
  var promise = null;

  // onResolved is optional and will pass through the received value once it does not exist or is not a function
  onResolved = typeof onResolved === 'function' ? onResolved : function (value) { return value; };
  // onRejected is optional, and continues its error when it does not exist or is not a function
  onRejected = typeof onRejected === 'function' ? onRejected : function (error) { throw error; };

  // The new promise state needs to be specific to x
  function resolvePromise(promise, x, resolve, reject) {
    // This is part of the Resolution Procedure section of the Promise/A+ specification

    // 2.3.1: If the Promise object and X reference the same object, reject the promise with a TypeError error
    // If two objects are the same object, then an infinite loop will be called, and an error will occur
    if (promise === x) {
      return reject(new TypeError('Chaining cycle detected for promise! '));
    }

    // 2.3.2: If x is a promise, the following should be used to determine its state
    if (x instanceof Promise) {
      // 2.3.2.1: If x is pending, then the promise must be pending until x is fulfillded or Rejected
      // 2.3.2.2: If x is a pity state, then the promise needs to use the same value to resolve
      // 2.3.2.3: If x is rejected, then promise needs the same reason reject
      if (x.status === 'pending') {
        x.then(function(value) {
          // Since x may still be a promise, this is called recursively
          resolvePromise(promise, value, resolve, reject);
        }, reject);
      } else {
        x.then(resolve, reject);
      }
      return;
    }

    // 2.3.3: If x is an object or function, thenable is an object or function with then functions
    // 2.3.4: If x is neither an object nor a function, use x directly to resolve the promise
    if((x ! = =null && typeof x === 'object') | |typeof x === 'function') {
      var isCalled = false;

      try {
        // 2.3.3.1: assigns x. teng to THEN
        var then = x.then;
        // 2.3.3.2: Reject x. Chen if x. Chen is rejected
        // 2.3.3.3: If then is a function, use x as this, with resolvePromise as the first argument and rejectPromise as the second
        if (typeof then === 'function') {
          // 2.3.3.3.1: If resolvePromise is called with a parameter value y, execute [[Resolve]](promise, y)
          // 2.3.3.3.2: If rejectPromise is invoked with a reason r, reject the promise with R
          then.call(x, function (y) {
            // 2.3.3.3.3: If resolvePromise and rejectPromise are called at the same time, or if they are called multiple times with the same arguments, then only the first one will be executed and the rest will be ignored
            if (isCalled) return;
            isCalled = true;
            return resolvePromise(promise, y, resolve, reject);
          }, function (r) {
            if (isCalled) return;
            isCalled = true;
            return reject(r);
          });
        } else {
          // 2.3.3.4: Resolve promise with x if then is not a functionresolve(x); }}catch(err) {
        // 2.3.3.3.4: An error is raised when calling then
        // 2.3.3.3.4.1: If resolvePromise and rejectPromise have already been invoked, ignore them
        // 2.3.3.3.4.2: Otherwise use ERR, reject promise
        if (isCalled) return;
        isCalled = true; reject(err); }}else{ resolve(x); }}function handlePromise(modifier, resolve, reject) {
    return function (value) {
      setTimeout(function() {
        try {
          var x = modifier(value);
          resolvePromise(promise, x, resolve, reject);
        } catch(err) { reject(err) } }); }}if (self.status === 'fulfilled') {
    promise = new Promise(function (resolve, reject) {
      handlePromise(onResolved, resolve, reject)(self.value);
    });
  } else if (self.status === 'rejected') {
    promise = new Promise(function (resolve, reject) {
      handlePromise(onRejected, resolve, reject)(self.reason);
    });
  } else {
    promise = new Promise(function (resolve, reject) {
      self.resolveCbs.push(handlePromise(onResolved, resolve, reject));
      self.rejectCbs.push(handlePromise(onRejected, resolve, reject));
    });
  }

  return promise;
}
Copy the code

At this point our Promise has almost been fulfilled. Let’s test whether our Promise meets the Promise/A+ specification.

test

Here we use Promises – Tests, which require an adapter to be implemented. Let’s implement one according to its requirements

Promise.deferred = function () {
  var global = {};

  var promise = new Promise(function (onResolve, onReject) {
    global.onResolve = onResolve;
    global.onReject = onReject;
  });

  var resolve = function (value) {
    global.onResolve(value);
  };

  var reject = function (reason) {
    global.onReject(reason);
  }

  return {
    promise,
    resolve,
    reject
  }
}
Copy the code

And then we export our Promise

module.exports = Promise;
Copy the code

Call our Promise to test

var promisesAplusTests = require('promises-aplus-tests');
var adapter = require('./promise');

promisesAplusTests(adapter);
Copy the code

Finally, regardless of performance, all of our promises passed the Promise/A+ tests.

Other articles

  • Principle and implementation of single routing
  • That’s all it takes to cross domains
  • Daily Curiosity – See how ES6 classes are implemented
  • React is implemented from zero
  • Font size for inline CSS elements
  • How to understand Genrator