The Promise object is used to represent the final completion (or failure) of an asynchronous operation and its resulting value.

Basic use of promise

const p = new Promise((resolve, reject) = > {
  setTimeout(() = > {
    console.log('do something... ');
    const value = 'some result';
    resolve(value);
  }, 1000);
});

p.then(
  value= > {
    console.log(value);
  },
  err= > {
    console.log(err); });// Promise can also be chained
new Promise((resolve, reject) = > {
  setTimeout(() = > {
    console.log('do something... ');
    const value = 'some result';
    resolve(value);
  }, 1000);
}).then(
  value= > {
    console.log(value);
  },
  err= > {
    console.log(err); });Copy the code

Implement A Promise that conforms to the Promise/A+ specification

Promises/A+

  • Promise defaults to a class that requires new, creates instances with a then method, and passes in an Executor executor argument in the new process
    • Pomise has three states, which are pending, successful and rejected. This is a pity.
    • Promise has a value attribute that describes the successful return value, and reason describes the reason for the failure
    • The logic in the promise will also fail if an exception occurs
    • When the promise state is pending, it can be replaced with pity or rejected. The process is irreversible, that is, the promise or rejected cannot be replaced with other states
    • The Executor executes immediately and accepts the resolve and Reject parameters, which are both function types. A call to resolve changes the promise to the fulfilled state, and a call to Reject changes the promise to the Rejected state.

Following the above analysis, we can write the first version of the original promise

// Define the promise state
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

class Promise {
  constructor(executor) {
    this.value = undefined;
    this.reason = undefined;
    this.status = PENDING;

    const resolve = value= > {
      // The pending state can only be changed to another state
      if (this.status ! == PENDING)return;
      this.status = FULFILLED;
      this.value = value;
    };

    const reject = reason= > {
      // The pending state can only be changed to another state
      if (this.status ! == PENDING)return;
      this.status = REJECTED;
      this.reason = reason;
    };

    try {
      executor(resolve, reject);
    } catch (e) {
      // An error occurred during executor executionreject(e); }}then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value);
    }
    if (this.status === REJECTED) {
      onRejected(this.reason); }}}Copy the code

However, the code implemented now is very fragile. It can only be called when the state becomes A pity or Rejected. But in Promises/A+ specification, THEN can be called at any time, and the same instance can be called several times. Therefore, we will change some code slightly. When storing pending state, we will call the onFulfilled and onRejected callback functions of THEN respectively. When calling resolve to change the promise state, we will remove the callback functions from the array and execute them successively. That would solve both of these problems.

// Define the promise state
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

class Promise {
  constructor(executor) {
    this.value = undefined;
    this.reason = undefined;
    this.status = PENDING;
    // Store the successful callback function
    this.onFulfilledCallbacks = [];
    // Store failed callback functions
    this.onRejectedCallbacks = [];

    const resolve = value= > {
      // The pending state can only be changed to another state
      if (this.status ! == PENDING)return;
      this.status = FULFILLED;
      this.value = value;
      this.onFulfilledCallbacks.forEach(cb= > cb(this.value));
    };

    const reject = reason= > {
      // The pending state can only be changed to another state
      if (this.status ! == PENDING)return;
      this.status = REJECTED;
      this.reason = reason;
      this.onRejectedCallbacks.forEach(cb= > cb(this.reason));
    };

    try {
      executor(resolve, reject);
    } catch (e) {
      // An error occurred during executor executionreject(e); }}then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value);
    }
    if (this.status === REJECTED) {
      onRejected(this.reason);
    }
    if (this.status === PENDING) {
      // It is possible to call then with neither success nor failure, storing the callback (publish-subscribe mode)
      this.onFulfilledCallbacks.push(onFulfilled);
      this.onRejectedCallbacks.push(onRejected); }}}Copy the code

This looks perfect, but Promises/A+ specification requires that the THEN method be called chained, and

  • The method passed in by THEN in a promise, which returns a result other than a promise, will be passed on to the success of the next THEN
  • If the method passed by THEN fails during execution, the next THEN fails
  • If the then method returns a promise, the state of the promise determines whether the next THEN succeeds or fails. The value of the success and the reason for the failure are based on the current promise

So here we modify the then method to allow a callback that does not return a promise:

then(onFulfilled, onRejected) {
    // To support chained calls, we return a new Promise instance
    const p = new Promise((resolve, reject) = > {
      if (this.status === FULFILLED) {
        try {
          const x = onFulfilled(this.value);
          resolve(x);
        } catch (e) {
          Reject error while executing callbackreject(e); }}if (this.status === REJECTED) {
        try {
          const x = onRejected(this.reason);
          resolve(x);
        } catch (error) {
          Reject error while executing callbackreject(e); }}if (this.status === PENDING) {
        this.onFulfilledCallbacks.push(() = > {
          try {
            const x = onFulfilled(this.value);
            resolve(x);
          } catch (e) {
            Reject error while executing callbackreject(e); }});this.onRejectedCallbacks.push(() = > {
          try {
            const x = onRejected(this.reason);
            resolve(x);
          } catch (e) {
            Reject error while executing callbackreject(e); }}); }});return p;
  }
Copy the code

Next, we will implement the most complicated case in promise, that is, when the onFulfilled or onRejected callback function returns a promise, we will define a resolvePromise function, This is specially used to deal with the remaining process after implementing onFulfilled or onRejected:

function resolvePromise(promise, x, resolve, reject) {
  // Use x to decide whether promise is resolve or reject

  // You can't wait to finish
  if (promise === x) {
    return reject(new TypeError(`TypeError: Chaining cycle detected for promise #<Promise> `));
  }

  // Consider: can we use x instanceof Promise here?
  // We need to consider that promises written by different people can be compatible with each other, so we need to implement the specification here and here to ensure that promises can be called to each other, so we can't use x instanceof promise here
  if ((typeof x === 'object'&& x ! = =null) | |typeof x === 'function') {
    // This then method may be the THEN in a promise implemented by someone else, and does not handle both success and failure calls,
    Use a single variable to identify only resolve or reject, not both
    let called = false;
    try {
      const then = x.then;
      if (typeof then === 'function') {
        // if x = {then: 111}
        // When the result is a Promise instance
        then.call(
          x,
          v= > {
            if (called) return;
            called = true;
            // The recursion determines whether the promise is still returned
            resolvePromise(promise, v, resolve, reject);
          },
          r= > {
            if (called) return;
            called = true; reject(r); }); }else {
        // Still normal valueresolve(x); }}catch (e) {
      "Then" is defined by defineProperty in other promise implementations. When x.chen is called, an error occurs
      /** Object.defineProperty(x, 'then', { get() { throw new Error() } }) */
      if (called) return;
      called = true; reject(e); }}else {
    // The execution returns a normal valueresolve(x); }}then(onFulfilled, onRejected) {
    // To support chained calls, we return a new Promise instance
    const p = new Promise((resolve, reject) = > {
      if (this.status === FULFILLED) {
        // try {
        // const x = onFulfilled(this.value);
        // // can't be written like this, because at this point p doesn't have a value, and we can't get an instance in new
        // // resolvePromise(p, x, resolve, reject);

        // // we will get the p instance in the next event loop tick
        // setTimeout(() => {
        // resolvePromise(p, x, resolve, reject);
        / /});
        // } catch (e) {
        // reject(e);
        // }
        setTimeout(() = > {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(p, x, resolve, reject);
          } catch(e) { reject(e); }}); }if (this.status === REJECTED) {
        setTimeout(() = > {
          try {
            const x = onRejected(this.reason);
            resolvePromise(p, x, resolve, reject);
          } catch(e) { reject(e); }}); }if (this.status === PENDING) {
        this.onFulfilledCallbacks.push(() = > {
          setTimeout(() = > {
            try {
              const x = onFulfilled(this.value);
              resolvePromise(p, x, resolve, reject);
            } catch(e) { reject(e); }}); });this.onRejectedCallbacks.push(() = > {
          setTimeout(() = > {
            try {
              const x = onRejected(this.reason);
              resolvePromise(p, x, resolve, reject);
            } catch(e) { reject(e); }}); }); }});return p;
  }
Copy the code

We used setTimeout above to get the returned promise instance in the next event loop tick, but setTimeout puts the callback into the macro task queue. We all know that native promises are put into a microtask queue, so we can use queueMicrotask instead of setTimeout.

If you call THEN, set the first parameter to undefined, and write onRejected only, then can implement the last Promise resolve value. Similarly, we can not write the second parameter in THEN and obtain the reason of the previous Promise reject in the later THEN to achieve the penetration effect.

The main implementation of a method can be passed layer by layer:

onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v= > v;
onRejected = typeof onRejected === 'function' ? onRejected : e= > { throw e };
Copy the code

We ended up with A full promise that Promises/A+ promise, Resolve, Promise. Reject, Promise. All, Promise. Race, and finally are not Promises/A+

Let’s also implement it here:

resolve

static resolve(value) {
  return new Promise((resolve, reject) = > {
    resolve(value);
  });
}
Copy the code

reject

static reject(value) {
  // Create a failed promise by default
  return new Promise((resolve, reject) = > {
    reject(value);
  });
}
Copy the code

catch

catch(errCallback) {
  return this.then(null, errCallback);
}
Copy the code

all

static all = function (promises) {
  let result = [];
  let times = 0;
  return new Promise((resolve, reject) = > {
    function processResult(data, index) {
      result[index] = data; // Mapping result
      if(++times == promises.length) { resolve(result); }}for (let i = 0; i < promises.length; i++) {
      let promise = promises[i];
      Promise.resolve(promise).then(data= >{ processResult(data, i); }, reject); }}); };Copy the code

race

static race = function (promises) {
  return new Promise((resolve, reject) = > {
    for (let i = 0; i < promises.length; i++) {
      let promise = promises[i];
      Promise.resolve(promise).then(resolve, reject); }}); };Copy the code

finally

finally(finallyCallback) {
  return this.then(
    data= > {
      Promise.resolve(finallyCallback()).then(() = > data);
    },
    err= > {
      return Promise.resolve(finallyCallback()).then(() = > {
        throwerr; }); }); }Copy the code

allSettled

static allSettled = function (promises) {
  let result = [];
  let times = 0;
  return new Promise((resolve, reject) = > {
    function processResult(data, index, status) {
      result[index] = { status, value: data };
      if(++times == promises.length) { resolve(result); }}for (let i = 0; i < promises.length; i++) {
      let promise = promises[i];
      Promise.resolve(promise).then(
        data= > {
          processResult(data, i, 'fulfilled');
        },
        err= > {
          processResult(err, i, 'rejected'); }); }}); };Copy the code

The code for the final version is as follows:

const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';

function resolvePromise(promise, x, resolve, reject) {
  if (promise === x) {
    return reject(new TypeError(`TypeError: Chaining cycle detected for promise #<Promise> `));
  }
  if ((typeof x === 'object'&& x ! = =null) | |typeof x === 'function') {
    let called = false;
    try {
      const then = x.then;
      if (typeof then === 'function') {
        then.call(
          x,
          v= > {
            if (called) return;
            called = true;
            resolvePromise(promise, v, resolve, reject);
          },
          r= > {
            if (called) return;
            called = true; reject(r); }); }else{ resolve(x); }}catch (e) {
      if (called) return;
      called = true; reject(e); }}else{ resolve(x); }}class Promise {
  constructor(executor) {
    this.value = undefined;
    this.reason = undefined;
    this.status = PENDING;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = value= > {
      // Here we add an out-of-specification logic to allow a resolution if the value is a promise
      if(value instanceof Promise) {// Recursively parse the value
         return value.then(resolve, reject)
      }
      if (this.status ! == PENDING)return;
      this.status = FULFILLED;
      this.value = value;
      this.onFulfilledCallbacks.forEach(cb= > cb(this.value));
    };

    const reject = reason= > {
      if (this.status ! == PENDING)return;
      this.status = REJECTED;
      this.reason = reason;
      this.onRejectedCallbacks.forEach(cb= > cb(this.reason));
    };

    try {
      executor(resolve, reject);
    } catch(e) { reject(e); }}then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v= > v;
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : e= > {
            throw e;
          };
    const p = new Promise((resolve, reject) = > {
      if (this.status === FULFILLED) {
        queueMicrotask(() = > {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(p, x, resolve, reject);
          } catch(e) { reject(e); }}); }if (this.status === REJECTED) {
        queueMicrotask(() = > {
          try {
            const x = onRejected(this.reason);
            resolvePromise(p, x, resolve, reject);
          } catch(e) { reject(e); }}); }if (this.status === PENDING) {
        this.onFulfilledCallbacks.push(() = > {
          queueMicrotask(() = > {
            try {
              const x = onFulfilled(this.value);
              resolvePromise(p, x, resolve, reject);
            } catch(e) { reject(e); }}); });this.onRejectedCallbacks.push(() = > {
          queueMicrotask(() = > {
            try {
              const x = onRejected(this.reason);
              resolvePromise(p, x, resolve, reject);
            } catch(e) { reject(e); }}); }); }});return p;
  }

  catch(errCallback) {
    return this.then(null, errCallback);
  }

  finally(finallyCallback) {
    return this.then(
      data= > {
        Promise.resolve(finallyCallback()).then(() = > data);
      },
      err= > {
        return Promise.resolve(finallyCallback()).then(() = > {
          throwerr; }); }); }static resolve(value) {
    return new Promise((resolve, reject) = > {
      resolve(value);
    });
  }

  static reject(value) {
    return new Promise((resolve, reject) = > {
      reject(value);
    });
  }

  static all = function (promises) {
    let result = [];
    let times = 0;
    return new Promise((resolve, reject) = > {
      function processResult(data, index) {
        result[index] = data; // Mapping result
        if(++times == promises.length) { resolve(result); }}for (let i = 0; i < promises.length; i++) {
        let promise = promises[i];
        Promise.resolve(promise).then(data= >{ processResult(data, i); }, reject); }}); };static race = function (promises) {
    return new Promise((resolve, reject) = > {
      for (let i = 0; i < promises.length; i++) {
        let promise = promises[i];
        Promise.resolve(promise).then(resolve, reject); }}); };static allSettled = function (promises) {
    let result = [];
    let times = 0;
    return new Promise((resolve, reject) = > {
      function processResult(data, index, status) {
        result[index] = { status, value: data };
        if(++times == promises.length) { resolve(result); }}for (let i = 0; i < promises.length; i++) {
        let promise = promises[i];
        Promise.resolve(promise).then(
          data= > {
            processResult(data, i, 'fulfilled');
          },
          err= > {
            processResult(err, i, 'rejected'); }); }}); }; }module.exports = Promise;
Copy the code

Finally, to test whether the code we wrote fully conforms to Promises/A+ specification, we can install the package Promises – aplus-Tests.

We also need to add some code for Promises – aplus-Tests:

// It needs to be exported to test compliance with the specification
Promise.deferred = function () {
  const dfd = {};
  dfd.promise = new Promise((resolve, reject) = > {
    dfd.resolve = resolve;
    dfd.reject = reject;
  });
  return dfd;
};
Copy the code

The last execution

npx promises-aplus-tests myPromise.js
Copy the code

You can see that we passed all the test cases