preface

Promise is a solution to asynchronous programming that is more reasonable and powerful than the traditional solution, Callback. The biggest benefit of promises is a clear separation of the code that executes and the code that processes the results in an asynchronously executed process. At present, there are many versions of handwritten Promise source code in the community, but it is not easy to understand for students who are not familiar with the Promise specification. In this article I will take you through the Promises/A+ specification and write 872 test cases that will pass Through Promises – APlus-Tests. After reading this article, you’ll finally feel how easy it is to make promises!

Normative interpretation

Promises/A + specification

1. The term

  • 1.1 A Promise is an object or function that has then methods.
  • 1.2 Thenable is the object or function that defines the then method.
  • 1.3 Value is any valid JavaScript value (including undefined, Thenable, or promise).
  • 1.4 Exceptions are values thrown using a throw statement.
  • 1.5 Reason is a value that indicates the Reason why the commitment was rejected.

2. Specification requirements

(Ps: The most important thing is to understand this section, and then follow the specification guidelines to implement the Promise.)

The promise object must be in one of three states: Pending, fulfilled, and rejected.

2.1. Promise state value

  • 2.1.1 When waiting, commitment: can be changed to completed or rejected state.
  • 2.1.2 When a promise is fulfilled, its state value can only be fulfilled, which is very depressing or rejected.

2.2. Promise. Then method

A promise must provide then methods to access its current or final value or reason. The promise.then method takes two arguments

promise.then(onFulfilled, onRejected)
Copy the code
  • 2.2.1 onFulfilled and onRejected are both optional parameters: If onFulfilled is not a function, it must be ignored. If onRejected is not a function, it must be ignored.
  • 2.2.2 If ondepressing is a function: It must be called after the promise is fulfilled, with the promise value as its first parameter. Never invoke the promise before it has been fulfilled. And it can’t be called more than once.
  • 2.2.3 If onRejected is a function, it must be called after the Promise is rejected, with the Promise’s Reason as its first argument. Never invoke a promise until it has been rejected. And it can’t be called more than once.
  • 2.2.4 Ensure that onFulfilled and onRejected are executed after the event loop of calling THEN and use the new stack. (Execute after the current round of events in the THEN method)
  • OnFulfilled and onRejected must be functions.
  • The 2.2.6 THEN method can be called multiple times on the same promise. If when the promise is fulfilled, all the corresponding Ondepressing callbacks must be executed in the order of their original calls to THEN. If a commitment is rejected, all the corresponding onRejected callbacks must be executed in the same order as their original calls to THEN.
  • 2.2.7 then method must return a promise (promise2 = promise1. Then (onFulfilled, onRejected);)
    • 2.2.7.1 If onFulfilled or onRejected returns x, run the Promise resolution function [[Resolve]](promise2, x).
    • 2.2.7.2 If onFulfilled or onRejected throws an exception E, promise2 must be rejected on the grounds of E.
    • 2.2.7.3 If ondepressing is not a function and promise1 is resolved, then promise2 must be resolved with the same value as promise1.
    • 2.2.7.4 If onRejected is not a function and promise1 is rejected, then promise2 must be rejected with the same reason as promisE1.

Code implementation:

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

class MyPromise {
  status = PENDING; // Initial state
  reason = null; // Failed value
  value = null; // Success value
  onFulfilledCallbacks = []; // Save callback successfully
  onRejectedCallbacks = []; // Store failed callback

  constructor(executor) {
    if (typeofexecutor ! = ="function") {
      throw new Error(
        `Uncaught TypeError: MyPromise resolver <The ${typeof executor}> is not a function`
      );
    }
    // The effect of resolve is to change the promise state into a big pity and perform the asynchronous task
    const resolve = (value) = > {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        // Clear the success callback
        this.onFulfilledCallbacks.forEach((fn) = >fn()); }};// Reject changes the promise state to Rejected and executes the asynchronous task
    const reject = (reason) = > {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        // Empty rejected callback functions
        this.onRejectedCallbacks.forEach((fn) = >fn()); }};// Use try and catch to catch errors in the executor and change the state of a Promise to a failure when an error is executed
    try {
      executor(resolve, reject);
    } catch(e) { reject(e); }}/** * then implements the logic * 1. The parameters in the then method are optional * 2. 3. OnFulfilled and onRejected must be called */ after the event loop of THEN
  then(onFulfilled, onRejected) {
    // Provide default arguments so that the value of then can be "penetrated"
    onFulfilled =
      typeof onFulfilled === "function" ? onFulfilled : (value) = > value;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (reason) = > {
            throw reason;
          };

    // The then method implements chain calls
    // Make sure onFulfilled and onRejected are followed by the event loop calling THEN and use the new stack. So you need to wrap a layer of queueMicrotask to perform tasks using the new stack
    const promise2 = new MyPromise((resolve, reject) = > {
      if (this.status === FULFILLED) {
        // This can be done using macro task mechanisms (such as setTimeout or setImmediate) or microtask mechanisms (such as MutationObserver or process.nexttick).
        // Here I use queueMicrotask to ensure that ondepressing and onRejected are executed asynchronously after the event loop call
        queueMicrotask(() = > {
          // If onFulfilled or onRejected throws an exception E,promise2 must be rejected and e will be the reason
          try {
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch(e) { reject(e); }}); }else if (this.status === REJECTED) {
        queueMicrotask(() = > {
          try {
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch(e) { reject(e); }}); }else if (this.status === PENDING) {
        // Save the callback function
        this.onFulfilledCallbacks.push(() = > {
          queueMicrotask(() = > {
            try {
              const x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch(e) { reject(e); }}); });this.onRejectedCallbacks.push(() = > {
          queueMicrotask(() = > {
            try {
              const x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch(e) { reject(e); }}); }); }});returnpromise2; }}Copy the code

2.3 Promise Resolution (resolvePromise)

The promise resolution process is an abstract operation that takes a promise and a value as input, which we represent as [[Resolve]](promise, x). If x is a Thenable, it tries to get promise to get the state of X, assuming that X behaves at least somewhat like Promise. Otherwise, it fulfills its promise with value X.

This handling of Thenables allows the promise to implement recursion.

To run [[Resolve]](promise, x), follow these steps:

  • 2.3.1 If the PROMISE instance and X point to the same object, the reason for rejecting the promise is T ypeError.
  • 2.3.2 If x is pending, the promise must remain pending until X is completed or rejected. If the promise is fulfilled with the same value when x is completed. If x is rejected, refuse the commitment for the same reason.
  • 2.3.3 Otherwise, if x is an object or function, try to get X. teng first.
    • 2.3.3.1 If the retrieval of attribute X. teng causes an exception E to be thrown, the promise is rejected for reason E.
    • 2.3.3.2 If then is a function, it is called with x as this, with resolvePromise as the first argument and rejectPromise as the second argument
    • 2.3.3.3 If resolvePromise is called with the value y, run [[Resolve]](promise, y).
    • 2.3.3.4 If a rejectPromise is invoked for reason r, reject the promise with r.
    • 2.3.3.5 If both resolvePromise and rejectPromise are called, or if multiple calls are made on the same parameter, the first call takes precedence and any further calls are ignored.
  • 2.3.4 If X is neither an object nor a function, fulfill (fulfill) with x

Code implementation:

/** * MyPromise returns a value as an argument to the next THEN method. If the then method returns its own MyPromise object, a loop will be called and an error will be reported * */
function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(
      new TypeError("Chaining cycle detected for promise #<MyPromise>")); }If both resolvePromise and rejectPromise are called, or if multiple calls are made to the same parameter, the first call takes precedence and the other calls are ignored.
  let called = false;
  //Otherwise, if x is an object or function
  if( x ! = =null &&
    (Object.prototype.toString.call(x) === "[object Object]" ||
      typeof x === "function")) {// If the result of the retrieved x.chen attribute is an exception E, use e as the reason, reject promise
    DefineProperty (x, 'then', get() {throw new Error('err')})
    // If then is a method, call x as this with the first argument resolvePromise and the second argument rejectPromise
    try {
      let then = x.then;
      if (typeof then === "function") {
        then.call(
          x,
          (y) = > {
            if (called) return;
            called = true;
            // Executes recursively, each time with a new promise instance
            resolvePromise(promise2, y, resolve, reject);
          },
          (r) = > {
            if (called) return;
            called = true; reject(r); }); }else {
        if (called) return;
        called = true; resolve(x); }}catch (e) {
      if (called) return;
      called = true; reject(e); }}else {
    Fulfill (fulfill) if x is neither an object nor a functionresolve(x); }}Copy the code

Realize the Promise. The resolve ()

The promise.resolve (value) method returns a Promise object resolved with the given value. If the value is a Promise, the promise is returned; If the value is thenable (that is, with the “then” method), the returned promise “follows “the thenable object and adopts its final state; Otherwise the returned promise will be fulfilled with this value. This function flattens out the multiple layers of nesting of promise-like objects.

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

Warning: Do not call promise.resolve on thenable that resolves to itself. This leads to infinite recursion because it tries to flatten infinitely nested promises.

let thenable = {
  then: (resolve, reject) = > {
    resolve(thenable)
  }
}

Promise.resolve(thenable)  // This creates an endless loop

Copy the code

Realize the Promise. Reject ()

The promise.reject () method returns a Promise object with a reason for the rejection.

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

Realize the Promise. Race ()

The promise.race (iterable) method returns a Promise that is resolved or rejected once a Promise in the iterator is resolved or rejected.

static race(promises) {
  return new Promise((resolve, reject) = > {
    for (let i in promises) {
      let value = promises[i];
      if (value && typeof value.then === "function") {
        value.then(resolve, reject);
      } else{ resolve(value); }}}); }Copy the code

Achieve the finally ()

The finally() method is used to specify actions that will be performed regardless of the final state of the Promise object. This method was introduced as a standard in ES2018.

finally(callback) {
  return this.then(
    (value) = > {
      return MyPromise.resolve(callback()).then(() = > value);
    },
    (reason) = > {
      return MyPromise.resolve(callback()).then(() = > {
        throwreason; }); }); }Copy the code

Implement the catch ()

The promise.prototype.catch () method is an alias for. Then (null, Rejection) or. Then (undefined, Rejection) that specifies the callback when an error occurs.

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

The final complete code


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

/** * MyPromise returns a value as an argument to the next THEN method. If the then method returns its own MyPromise object, a loop will be called and an error will be reported * */
function resolvePromise(promise2, x, resolve, reject) {
  //If promise and x refer to the same object, reject promise with a TypeError as the reason.
  if (promise2 === x) {
    return reject(
      new TypeError("Chaining cycle detected for promise #<MyPromise>")); }If both resolvePromise and rejectPromise are called, or if multiple calls are made to the same parameter, the first call takes precedence and the other calls are ignored.
  let called = false;
  //Otherwise, if x is an object or function
  if( x ! = =null &&
    (Object.prototype.toString.call(x) === "[object Object]" ||
      typeof x === "function")) {// If the result of the retrieved x.chen attribute is an exception E, use e as the reason, reject promise
    DefineProperty (x, 'then', get() {throw new Error('err')})
    // If then is a method, call x as this with the first argument resolvePromise and the second argument rejectPromise
    try {
      let then = x.then;
      if (typeof then === "function") {
        then.call(
          x,
          (y) = > {
            if (called) return;
            called = true;
            // Executes recursively, each time with a new promise instance
            resolvePromise(promise2, y, resolve, reject);
          },
          (r) = > {
            if (called) return;
            called = true; reject(r); }); }else {
        if (called) return;
        called = true; resolve(x); }}catch (e) {
      if (called) return;
      called = true; reject(e); }}else {
    Fulfill (fulfill) if x is neither an object nor a functionresolve(x); }}class MyPromise {
  status = PENDING; // Initial state
  reason = null; // Failed value
  value = null; // Success value
  onFulfilledCallbacks = []; // Save callback successfully
  onRejectedCallbacks = []; // Store failed callback

  constructor(executor) {
    if (typeofexecutor ! = ="function") {
      throw new Error(
        `Uncaught TypeError: MyPromise resolver <The ${typeof executor}> is not a function`
      );
    }
    const resolve = (value) = > {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        // Clear the current success callback
        this.onFulfilledCallbacks.forEach((fn) = >fn()); }};const reject = (reason) = > {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        // Clear the current failed callback function
        this.onRejectedCallbacks.forEach((fn) = >fn()); }};// Use try and catch to catch errors in the executor and change the state of a Promise to a failure when an error is executed
    try {
      executor(resolve, reject);
    } catch(e) { reject(e); }}static resolve(value) {
    return new MyPromise((resolve, reject) = > {
      resolve(value);
    });
  }

  static reject(reason) {
    return new MyPromise((resolve, reject) = > {
      reject(reason);
    });
  }

  static all(promises) {
    if (!Array.isArray(promises)) {
      const type = typeof promises;
      return new TypeError(`TypeError: ${type} ${promises} is not iterable`);
    }

    return new MyPromise((resolve, reject) = > {
      let resultArr = [];
      let orderIndex = 0;
      const processResultByKey = (value, index) = > {
        resultArr[index] = value;
        if(++orderIndex === promises.length) { resolve(resultArr); }};for (let i in promises) {
        const value = promises[i];
        if (value && typeof value.then === "function") {
          value.then((value) = > {
            processResultByKey(value, i);
          }, reject);
        } else{ processResultByKey(value, i); }}}); }static race(promises) {
    return new Promise((resolve, reject) = > {
      for (let i in promises) {
        let value = promises[i];
        if (value && typeof value.then === "function") {
          value.then(resolve, reject);
        } else{ resolve(value); }}}); }/** * then implements the logic * 1. The parameters in the then method are optional * 2. Execute onRejected when the state is REJECTED and return a new MyPromise for the chain call */
  then(onFulfilled, onRejected) {
    // Provide default arguments so that the value of then can be "penetrated"
    onFulfilled =
      typeof onFulfilled === "function" ? onFulfilled : (value) = > value;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (reason) = > {
            throw reason;
          };

    // The then method implements chain calls
    // Make sure onFulfilled and onRejected are followed by the event loop calling THEN and use the new stack. So you need to wrap a layer of queueMicrotask to perform tasks using the new stack
    const promise2 = new MyPromise((resolve, reject) = > {
      if (this.status === FULFILLED) {
        // In the Promise /A+ specification, there is A requirement to ensure that the onFulfilled and onRejected are executed asynchronously after the event loop call and there is A new stack.
        // This can be done using macro task mechanisms (such as setTimeout or setImmediate) or microtask mechanisms (such as MutationObserver or process.nexttick).
        // Here I use queueMicrotask to ensure that ondepressing and onRejected are executed asynchronously after the event loop call
        queueMicrotask(() = > {
          // If onFulfilled or onRejected throws an exception E,promise2 must be rejected and e will be the reason
          try {
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch(e) { reject(e); }}); }else if (this.status === REJECTED) {
        queueMicrotask(() = > {
          try {
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch(e) { reject(e); }}); }else if (this.status === PENDING) {
        // Save the callback function
        this.onFulfilledCallbacks.push(() = > {
          queueMicrotask(() = > {
            try {
              const x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch(e) { reject(e); }}); });this.onRejectedCallbacks.push(() = > {
          queueMicrotask(() = > {
            try {
              const x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch(e) { reject(e); }}); }); }});return promise2;
  }

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

    The //finally() method is used to specify actions that will be performed regardless of the final state of the MyPromise object. This method was introduced as a standard in ES2018.
  // Whether MyPromise is fulfilled or rejected, the callback function will be implemented.
  finally(callback) {
    return this.then(
      (value) = > {
        return MyPromise.resolve(callback()).then(() = > value);
      },
      (reason) = > {
        return MyPromise.resolve(callback()).then(() = > {
          throwreason; }); }); }}Copy the code

Use Promises – aplus-Tests to test the written promise source code

  1. We use NPM init-y to create a promise project.
  2. npm i promises-aplus-tests -D
  3. The package.json file configures the script execution part
"scripts": {
    "test": "promises-aplus-tests index.js"  // The index.js file here is the source file you want to test
},
Copy the code
  1. Add the following code to the handwritten index.js file (the file name can be customized) and change it to your own promise name
MyPromise.defer = MyPromise.deferred = function () {
  let dfd = {};
  dfd.promise = new MyPromise((resolve, reject) = > {
    dfd.resolve = resolve;
    dfd.reject = reject;
  });
  return dfd;
};

module.exports = MyPromise;
Copy the code
  1. npm run test

Finally, you should see the test results:

The above represents our handwritten source code through the specification requirements.

conclusion

In fact, the premise of handwritten Promise is to be familiar with the specification, in accordance with the guidance of the specification to write the source code is OK. It should be noted that in the process of chain invocation to implement THEN, ondepressing or onRejected can be called only after the completion of promise2 instantiation to obtain the value X, and the type of value X needs to be further determined (that is, the implementation logic of resolvePromise method).