Author: Hanpeng_Chen

Public account: Front-end geek technology

Promises are at the heart of asynchronous programming in JavaScript and a common question in front-end interviews. The basic use of promises won’t be covered here, but let’s implement A Promise that conforms to the Promise/A+ specification.

Before we start writing code, we need to know what the Promise/A+ specification contains.

Promise/A + specification

promisesaplus.com/

Here is the official address of the Promise/A+ specification, an English version that you can click to read. Let me illustrate the key points in the specification:

The term

  1. A promise is an object or function with then methods that behave according to the specification.

  2. Thenable is an object or function that defines the then method.

  3. Value can be any valid JavaScript value, including undefined, thenable, and promise.

  4. Exception is an exception that can be thrown with a throw statement in a Promise.

  5. Reason is a reject reason returned in a Promise, reject.

state

    1. A Promise has three states: Pending, depressing and Rejected.
    1. When the state is pending, it can be replaced with pity or Rejected.
    1. This is a big pity. When the state is fulfilled, it cannot be changed into other states again and a value that can no longer be changed must be returned.
    1. When the state is Rejected, it cannot be changed to other states. There must be a reason for the promise to be rejected, and the reason value cannot be changed.

Then method

A Promise must have a THEN method to access the final result.

The then method takes two arguments:

promise.then(onFulfilled, onRejected)
Copy the code

OnFulfilled and onRejected are both optional parameters. Both onFulfilled and onRejected must be function types.

If onFulfilled is a function, onFulfilled must be called when the promise becomes a pity, and the parameter is the promise’s value. OnFulfilled can only be called once.

If onRejected is a function, you must call onRejected when the promise becomes Rejected. The argument is the promise’s reason. OnRejected can only be called once.

The THEN method can be called multiple times by a Promise, and must return a Promise object.

If the promise becomes a fulfilled state, all onFulfilled callbacks need to be executed in the order of then

If the Promise becomes the Rejected state, all onRejected callbacks need to be executed in the order of THEN

This is the main part of the Promise/A+ specification, so let’s implement A Promise together.

Realize the Promise

The constructor

The Promise constructor accepts an executor function that calls resolve and reject with its two arguments after performing a synchronous or asynchronous operation.

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";


function Promise(executor) {
  let self = this;
  self.status = PENDING;
  self.data = undefined; / / the value of the Promise
  self.onResolvedCallback = []; // The Promise resolve callback
  self.onRejectedCallback = []; // The Promise rejected callback function set

  function resolve(value) {}function reject(reason) {}try {
    executor(resolve, reject)
  } catch(e) {
    reject(e)
  }
}
Copy the code

The Promise constructor defines resolve and reject.

As we know from the specification, the resolve and reject functions mainly return the value or reason of the corresponding state, and change the state inside the Promise from pending to the corresponding state, which is irreversible after the state changes.

How to implement these two functions can be seen in the following code:

function Promise(executor) {...function resolve(value) {
    if (self.status === PENDING) {
      self.status = FULFILLED;
      self.data = value;
      for(let i = 0; i < self.onResolvedCallback.length; i++) {
        self.onResolvedCallback[i](value)
      }
    }
  }
  function reject(reason) {
    if (self.status === PENDING) {
      self.status = REJECTED;
      self.data = reason;
      for(let i = 0; i < self.onRejectedCallback.length; i++) { self.onRejectedCallback[i](reason); }}}... }Copy the code

Implementation of then methods

The then method receives two parameters, which are onFulfilled, the successful callback of the Promise, and onFulfilled, the failed callback.

If the promise is fulfilled when the THEN is called, ondepressing will be performed and the value will be passed in as the parameter.

If the promise has failed, execute onRejected and pass reason as the failed argument.

If the state of the Promise is pending, ondepressing and onRejected functions need to be stored. After the state is determined, the corresponding functions will be executed successively (release and subscribe).

A promise can be then multiple times, and the then method of a promise returns a promise

We then method implementation of the idea through, the following to see its implementation code:

Promise.prototype.then = function(onFulfilled, onRejected) {
  let self = this;
  let promise2;

  // According to the specification, if the argument to then is not function, it needs to be ignored
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value;
  onRejected = typeof onRejected === 'function' ? onRejected : reason= > { throw reason };

  if (self.status === FULFILLED) {
    // If the promise state is fulfilled, call onFulFilled, but it may be thrown during code execution, so wrap it in a try/catch block
    return promise2 = new Promise(function(resolve, reject) {
      try {
        let x = onFulfilled(self.data)
        if (x instanceof Promise) {
          x.then(resolve, reject)
        }
        resolve(x)
      } catch(e) {
        reject(e)
      }
    })
  }
  if (self.status === REJECTED) {
    return promise2 = new Promise(function(resolve, reject) {
      try {
        let x = onRejected(self.data);
        if (x instanceof Promise) {
          x.then(resolve, reject)
        }
      } catch(e) {
        reject(e)
      }
    })
  }
  if (self.status === PENDING) {
    return promise2 = new Promise(function(resolve, reject) {
      self.onResolvedCallback.push(function(value) {
        try {
          let x = onFulfilled(self.data);
          if (x instanceof Promise) {
            x.then(resolve, reject)
          }
        } catch(e) {
          reject(e)
        }
      })
      self.onRejectedCallback.push(function(reason) {
        try {
          let x = onRejected(self.data)
          if (x instanceof Promise) {
            x.then(resolve, reject)
          }
        } catch(e) {
          reject(e)
        }
      })
    })
  }
}
Copy the code

At this point, A THEN method that conforms to the Promise/A+ specification is basically implemented.

To optimize the

In the Promise/A+ specification, the onFulfilled and onRejected functions need to be called asynchronously.

And it is mentioned in the specification that different promises should be supported for interaction. For details on different Promise interaction, click the link below to view:

promisesaplus.com/#point-46

Based on the above two points, we optimize the method of implementing promises in the then method implemented above, and add setTimeout(fn, 0) to resolve or reject promises.

The optimized complete code is as follows:

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function Promise(executor) {
  let self = this;
  self.status = PENDING;
  self.data = undefined; / / the value of the Promise
  self.onResolvedCallback = []; // The Promise resolve callback
  self.onRejectedCallback = []; // The Promise rejected callback function set

  function resolve(value) {
    if (value instanceof Promise) {
      return value.then(resolve, reject);
    }
    setTimeout(function () {
      if (self.status === PENDING) {
        self.status = FULFILLED;
        self.data = value;
        for (let i = 0; i < self.onResolvedCallback.length; i++) { self.onResolvedCallback[i](value); }}},0);
  }
  function reject(reason) {
    setTimeout(function () {
      if (self.status === PENDING) {
        self.status = REJECTED;
        self.data = reason;
        for (let i = 0; i < self.onRejectedCallback.length; i++) { self.onRejectedCallback[i](reason); }}},0);
  }

  try {
    executor(resolve, reject);
  } catch(e) { reject(e); }}Promise.prototype.then = function (onFulfilled, onRejected) {
  let self = this;
  let promise2;

  // According to the specification, if the argument to then is not function, it needs to be ignored
  onFulfilled =
    typeof onFulfilled === "function" ? onFulfilled : function(v) {
      return v;
    };
  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : function(r) {
        throw r;
      };

  if (self.status === FULFILLED) {
    // If the promise state is fulfilled, call onFulFilled, but it may be thrown during code execution, so wrap it in a try/catch block
    return promise2 = new Promise(function (resolve, reject) {
      setTimeout(function(){
        try {
          let x = onFulfilled(self.data);
          resolvePromise(promise2, x, resolve, reject)
        } catch(e) { reject(e); }})}); }if (self.status === REJECTED) {
    return promise2 = new Promise(function (resolve, reject) {
      setTimeout(function(){
        try {
          let x = onRejected(self.data);
          resolvePromise(promise2, x, resolve, reject)
        } catch(e) { reject(e); }})}); }if (self.status === PENDING) {
    return promise2 = new Promise(function (resolve, reject) {
      self.onResolvedCallback.push(function (value) {
        try {
          let x = onFulfilled(self.data);
          resolvePromise(promise2, x, resolve, reject)
        } catch(e) { reject(e); }}); self.onRejectedCallback.push(function (reason) {
        try {
          let x = onRejected(self.data);
          resolvePromise(promise2, x, resolve, reject)
        } catch(e) { reject(e); }}); }); }};function resolvePromise(promise2, x, resolve, reject) {
  let then;
  let thenCalledOrThrow = false;
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'))}if (x instanceof Promise) {
    if (x.status === PENDING) {
      x.then(function(v) {
        resolvePromise(promise2, v, resolve, reject)
      }, reject)
    } else {
      x.then(resolve, reject)
    }
    return
  }
  if((x ! = =null) && (typeof x === 'object' || typeof x === 'function')) {
    try {
      then = x.then;
      if (typeof then === 'function') {
        then.call(x, function(y) {
          if (thenCalledOrThrow) return; // it has already been called
          thenCalledOrThrow = true;
          return resolvePromise(promise2, y, resolve, reject)
        }, function(r) {
          if (thenCalledOrThrow) return;
          thenCalledOrThrow = true;
          returnreject(r); })}else {
        resolve(x)
      }
    } catch(e) {
      if (thenCalledOrThrow) return;
      thenCalledOrThrow = true;
      returnreject(e); }}else {
    resolve(x)
  }
}

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

module.exports = Promise;
Copy the code

test

There are special test scripts to test whether we write promises that conform to the Promise/A+ specification.

First expose a Deferred method in the Promise implementation code:

Promise.defer = Promise.deferred = function(){
  let dfd = {};
  dfd.promise = new Promise(function(resolve, reject) {
    dfd.resolve = resolve;
    dfd.reject = reject;
  });
  return dfd;
}
Copy the code

Perform NPM installation and execute test commands in the Promise source directory:

npm i -g promises-aplus-tests

promises-aplus-tests Promise.js
Copy the code

Promises – Aplus-Tests contains 872 test cases. As you can see from the execution results above, our handwritten Promise passes through all the use cases.

Promise implementation of other methods

Native Promises also provide other methods, such as:

  • Promise.resolve()
  • Promise.reject()
  • Promise.prototype.catch()
  • Promise.prototype.finally()
  • Promise.all()
  • Promise.race()
  • .

In the interview is also often encountered by the interviewer to handwritten implementation of these methods, let’s take a look at these methods how to achieve:

Promise.resolve

The promise.resolve (value) method returns a resolved Promise object for a given value.

  • If value is a Thenable object, the returned promise will “follow” the Thenable object and adopt its final state

  • If the value passed in is itself a Promise object, promise.resolve will return the promise object unchanged.

  • Otherwise, a Promise object with that value as a success status is returned directly.

Promise.resolve = function (value) {
  if (value instanceof Promise) {
    return value
  }
  return new Promise((resolve, reject) = > {
    if (value && typeof value === 'object' && typeof value.then === 'function') {
      setTimeout(() = > {
        value.then(resolve, reject)
      })
    } else {
      resolve(value)
    }
  })
}
Copy the code

Promise.reject

Reject () differs from promise.resolve in that the arguments to the promise.reject () method are left as reject arguments to subsequent methods.

Promise.reject = function(reason) {
  return new Promise((resolve, reject) = > {
    reject(reason)
  })
}
Copy the code

Promise.prototype.catch

Promise.prototype.catch is a special then method used to specify an error callback

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

Promise.prototype.finally

Success or failure will always go to finally, and after finally, can continue then. And passes the value exactly as it should to the subsequent then.

Promise.prototype.finally = function(cb) {
  return this.then((value) = > {
    return Promise.resolve(cb()).then(() = > {
      return value
    })
  }, (err) = > {
    return Promise.resolve(cb()).then(() = > {
      throwerr; })})}Copy the code

Promise.all

Promise. All (Promises) return a Promise object

If the argument passed in is an empty iterable, then the promise callback completes (resolve), and only then, is executed synchronously; all else is returned asynchronously.

If the parameters passed in do not contain any promises, an asynchronous completion is returned.

Promises all promises in Promises are made when promises are “made” or when arguments do not contain promises are called back.

If one of the arguments fails, the promise object returned by promise.all fails

In any case, promise.all returns an array as the result of the completion state of the Promise

Promise.all = function(promises) {
  promises = Array.from(promises); // Convert an iterable to an array
  return new Promise((resolve, reject) = > {
    let index = 0;
    let result = [];
    if (promises.length === 0) {
      resolve(result);
    } else {
      function processValue(i, value) {
        result[i] = value;
        if(++index === promises.length) { resolve(result); }}for (let i = 0; i < promises.length; i++) {
        Promise.resolve(promises[i]).then((data) = > {
          processValue(i, data);
        }, (err) = > {
          reject(err);
          return; })}}})}Copy the code

Promise.race

The promise. race function returns a Promise that will be completed in the same way as the first Promise passed. It may be a high degree of completion or rejection, depending on which of the two is the first.

If the passed array of parameters is empty, the returned promise will wait forever.

If the iteration contains one or more non-promise values and/or resolved/rejected promises, promise.race resolves to the first value found in the iteration.

Promise.race = function(promises) {
  promises = Array.from(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; })}}})}Copy the code

conclusion

The first time you write it, you’ll encounter a lot of problems, but if you write a few times against the spec and summarize it yourself, you’ll soon be able to write the source code that passes the test.

If you can write code that complies with the PromiseA+ specification, I think you’ll be able to answer all the Promise questions in the interview.

This article all source code: handwriting-promise

If you find this helpful:

1. Click “like” to support it, so that more people can see this article

2, pay attention to the public account: front-end geek technology, we learn and progress together.