Step1: start with constructors

Consider the following three scenarios, where we typically do not resolve directly, but execute the asynchronous task in the Promise constructor and then resolve or Reject on the asynchronous task callback. Alternatively, an exception is thrown by a throw in the constructor constructor.

// call resolve
new MyPromise((resolve, reject) = > {
  resolve("helloworld");
});

// call reject
new MyPromise((resolve, reject) = > {
  reject("111");
});

new MyPromise((resolve, reject) = > {
  throw new Error("test try catch");
});
Copy the code

The states of the above three promises are transferred from Pending to pity, Rejected and Rejected respectively.

So the logic of the constructor is:

  1. Receiving a functionexcutorAs a parameter
  2. Initialize the Pending state
  3. throughtry... catchThe parcelexcutorExecute, executeexcutorWhen the incomingresolveandrejectMethods.
  4. resolve,rejectMethod to modify the state of a Promise

Therefore, according to ES6’s class syntax, it is easy to write code like this:

const PromiseStatus = {
  Pending: "PENDING".FulFilled: "FULFILLED".Rejected: "REJECTED"
};

class MyPromise {
  constructor(excutor) {
    this.status = PromiseStatus.Pending;
    this.value = undefined;
    this.reason = undefined;

    const resolve = (value) = > {
      setTimeout(() = > {
        if (this.status === PromiseStatus.Pending) {
          this.status = PromiseStatus.FulFilled;
          // Other logic}}); };const reject = (reason) = > {
      setTimeout(() = > {
        if (this.status === PromiseStatus.Pending) {
          this.status = PromiseStatus.Rejected;
          // Other logic}}); };try {
      excutor(resolve, reject);
    } catch(error) { reject(error); }}}Copy the code

In this case, Step1 is done. It is currently possible to generate a Promise instance through the constructor, but there is no then method, and no other logic is executed after resolve and Reject. Don’t worry. Let’s take it one step at a time.

Step2: Simplethenmethods

So just to review how do we use the then method in general? Typically, we perform an asynchronous operation with a Promise, and then(callback) process the value returned by the Promise. In addition, chain calls to THEN are often used. Again, a Promise may bind multiple THEN methods.

// chain call
new MyPromise((resolve) = > {
  resolve("a");
})
  .then((val) = > {
    console.log(val);
    return val + "b";
  })
  .then((val) = > {
    console.log(val);
    return val + "c";
  })
  .then((val) = > {
    console.log(val);
  });

// The same Promise, then can be executed multiple times
const promise = new MyPromise((resolve) = > {
  resolve("a");
});

promise.then((val) = > console.log("1:", val));
promise.then((val) = > console.log("2:", val));
Copy the code

In addition to the normal logic of asynchronous tasks, don’t forget that the then method can pass in a second argument, which is also a method that takes an argument as an argument and passes the argument to the method when a Promise executes an exception.

new MyPromise((resolve) = > {
  resolve("a");
})
  .then((val) = > {
    throw new Error(val);
  })
  .then(
    (val) = > {
      console.log("not run");
      return "not run";
    },
    (error) = > {
      return "handle error";
    }
  )
  .then((val) = > {
    console.log(val);
  });
Copy the code

So, let’s summarize a few points for implementing the THEN method:

  1. Support chain call, support value pass through
  2. The same Promise,thenIt can be executed multiple times. When the Promise state changes from Pending to Fulfilled or Rejected, the corresponding callback will be executed according to the binding order.

Before we do that, let’s define the then method:

then(onFulfilled, onRejected): Promise<any>;

onFulfilled(x: any): any;

onRejected(x: any): any;
Copy the code

Chain calls

When we call then, both arguments are optional. In order to pass values through, we need to check whether the two arguments passed in are functions. If not, we need to give it a default processing method:

  then(onFulfilled, onRejected) {
    onFulfilled =
      typeof onFulfilled === "function" ? onFulfilled : (value) = > value;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (reason) = > {
            throw reason;
          };
  }
Copy the code

OnFulfilled is the default processing way which is to pass through the value directly, while onRejected is to continue to throw the data out.

A chained call returns a new Promise each time it executes a THEN, and then instantiates a promise2:

  then(onFulfilled, onRejected) {
    onFulfilled =
      typeof onFulfilled === "function" ? onFulfilled : (value) = > value;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (reason) = > {
            throw reason;
          };

    const promise2 = new MyPromise((resolve, reject) = > {
        try {
            const x = onFulfilled(this.value);
            resolve(x);
        } catch(error) { reject(error); }});return promise2;
  }
Copy the code

thenSupport multiple calls

The same Promise can call THEN multiple times to bind different callbacks, so there must be an array to store onFulfilled and onRejected.

The execution of THEN method can be divided into three cases:

  1. The state of the Promise is Pending, so executethen(onFulfilled, onRejected)When theonFulfilledandonRejectedMethods are placed in an array and wait for the Promise state to changeFulfilledorRejected, and then execute them in sequence.
  2. This is a big pity. The state of the Promise is Fulfilled directlyonFulfilledMethods.
  3. The Promise is in the Rejected stateonRejectedMethods.

In this case, we first add two arrays to store the arguments passed by the then method:

this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
Copy the code

Resolve and reject are methods that convert the Promise state, so the constructor should look something like this:

  constructor(excutor) {
    this.status = PromiseStatus.Pending;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    this.value = undefined;
    this.reason = undefined;

    // When the promise is successfully implemented, all onFulfilled shall be called back successively according to its registration order
    const resolve = (value) = > {
      // The state of a promise can only be changed once
      if (this.status === PromiseStatus.Pending) {
        this.status = PromiseStatus.FulFilled;
        this.value = value;
        setTimeout(() = > {
          this.onFulfilledCallbacks.forEach((callback) = >{ callback(value); }); }); }};// When a promise is rejected, all onRejected are called back in the order in which they were registered
    const reject = (reason) = > {
      // The state of a promise can only be changed once
      if (this.status === PromiseStatus.Pending) {
        this.status = PromiseStatus.Rejected;
        this.reason = reason;
        setTimeout(() = > {
          this.onRejectedCallbacks.forEach((callback) = >{ callback(reason); }); }); }};try {
      excutor(resolve, reject);
    } catch(error) { reject(error); }}Copy the code

Then, we add the following logic to the constructor of promise2 based on the different states in which the THEN method is executed to distinguish between the different states performing different operations:

const promise2 = new MyPromise((resolve, reject) = > {
  _resolve = resolve;
  _reject = reject;

  if (this.status === PromiseStatus.Pending) {
    this.onFulfilledCallbacks.push(onFulfilledCallback);
    this.onRejectedCallbacks.push(onRejectedCallback);
  }

  if (this.status === PromiseStatus.FulFilled) {
    setTimeout(() = > onFulfilledCallback(this.value));
  }

  if (this.status === PromiseStatus.Rejected) {
    setTimeout(() = > onRejectedCallback(this.reason)); }});return promise2;
Copy the code

You might notice: what are onCallledCallback and onRejectedCallback? They are based on the encapsulation of onFulfilled and onRejected. Why do you need this encapsulation? Let’s look at this example:

new Promise((resolve, reject) = > {
  reject("a");
})
  .then(
    (val) = > {
      console.log("not run");
      return "handle then";
    },
    (error) = > {
      console.log('error:', error);
      return "handle error";
    }
  )
  .then((val) = > {
    console.log(val);
  });
// error: a
// handler error
Copy the code

As can be seen, after onRejected is processed, the Promise state returned by the THEN method is Fulfilled, so the last THEN outputs the logic of Handle Error. In fact, only the onFulfilled and onRejected throw the error, the returned Promise will be changed from Pending to the Fulfilled state. Otherwise, the Promise will be replaced by the Fulfilled state. Therefore, the implementation of onCallledCallback and onRejectedCallback is as follows:

    let _resolve;
    let _reject;

    const onFulfilledCallback = (value) = > {
      try {
        const x = onFulfilled(value);
        _resolve(x);
      } catch(error) { _reject(error); }};const onRejectedCallback = (value) = > {
      try {
        const x = onRejected(value);
        _resolve(x);
      } catch(error) { _reject(error); }};Copy the code

The _resolve and _reject methods are assigned when promise2 is instantiated, so the complete Promise code implemented so far looks like this:

class MyPromise {
  constructor(excutor) {
    this.status = PromiseStatus.Pending;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    this.value = undefined;
    this.reason = undefined;

    // When the promise is successfully implemented, all onFulfilled shall be called back successively according to its registration order
    const resolve = (value) = > {
      // The state of a promise can only be changed once
      if (this.status === PromiseStatus.Pending) {
        this.status = PromiseStatus.FulFilled;
        this.value = value;
        setTimeout(() = > {
          this.onFulfilledCallbacks.forEach((callback) = >{ callback(value); }); }); }};// When a promise is rejected, all onRejected are called back in the order in which they were registered
    const reject = (reason) = > {
      // The state of a promise can only be changed once
      if (this.status === PromiseStatus.Pending) {
        this.status = PromiseStatus.Rejected;
        this.reason = reason;
        setTimeout(() = > {
          this.onRejectedCallbacks.forEach((callback) = >{ callback(reason); }); }); }};try {
      excutor(resolve, reject);
    } catch(error) { reject(error); }}then(onFulfilled, onRejected) {
    onFulfilled =
      typeof onFulfilled === "function" ? onFulfilled : (value) = > value;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (reason) = > {
            throw reason;
          };

    let _resolve;
    let _reject;

    const onFulfilledCallback = (value) = > {
      try {
        const x = onFulfilled(value);
        _resolve(x);
      } catch(error) { _reject(error); }};const onRejectedCallback = (value) = > {
      try {
        const x = onRejected(value);
        _resolve(x);
      } catch(error) { _reject(error); }};const promise2 = new MyPromise((resolve, reject) = > {
      _resolve = resolve;
      _reject = reject;

      if (this.status === PromiseStatus.Pending) {
        this.onFulfilledCallbacks.push(onFulfilledCallback);
        this.onRejectedCallbacks.push(onRejectedCallback);
      }

      if (this.status === PromiseStatus.FulFilled) {
        setTimeout(() = > onFulfilledCallback(this.value));
      }

      if (this.status === PromiseStatus.Rejected) {
        setTimeout(() = > onRejectedCallback(this.reason)); }});returnpromise2; }}Copy the code

Execute the test code again and the results are as expected:

new MyPromise((resolve) = > {
  resolve("a");
})
  .then((val) = > {
    console.log(val);
    return val + "b";
  })
  .then((val) = > {
    console.log(val);
    return val + "c";
  })
  .then((val) = > {
    console.log(val);
  });
// a
// ab
// abc

const promise = new MyPromise((resolve) = > {
  resolve("a");
});

promise.then((val) = > console.log("1:", val));
promise.then((val) = > console.log("2:", val));

// 1:a
// 2:a
Copy the code

However, if we return a Promise in THEN or a Promise in resolve, the output is not as expected:

new MyPromise((resolve) = > {
  resolve("a");
})
  .then((val) = > {
    return new MyPromise((resolve) = > resolve(val));
  })
  .then((val) = > {
    console.log("is promise, not a:", val);
  });
Copy the code

The expected result would be is A, but we’re getting is promise, not a. Mainly because the promises we currently implement do not support the resolvePromise process, this part is the most difficult part of implementing promises that comply with the Promise A+ specification, so we will discuss how to understand ResolvePromises in detail, rather than memorizing them.

Step3: resolvePromise

Understand why there areresolvePromise

Before writing a resolvePromise, we need to understand why this process is needed. From the example above, if we explicitly return a Promise from then, then since our THEN method originally returns a Promise object, if we do not, we will return a nested Promise. That’s why it prints “Is promise,” not a.

According to the PromiseA+ specification, if x is returned by then, x can be either a Promise object or a Thenable object (an object or function that has then methods). Therefore, a resolvePromise is needed to obtain the final state, or final value, of X.

Take a look at this example:

new Promise((resolve) = > {
  resolve();
})
  .then(() = > {
    // Do a bunch of logic
    return new Promise((resolve) = > {
      // Do a bunch of logic
      setTimeout(() = > {
        resolve("final");
      }, 1000);
    });
  })
  .then((val) = > {
    console.log("val:", val);
  });

Copy the code

The onFulfilled method in the first THEN method explicitly returns a Promise object, which we’ll call pA. But when we implement the THEN method, we know that the THEN method returns a Promise object, and we become pB. The state of pA and pB should be pB(pA).

If not handled by resolvePromise, and if we need to continue processing the pA state in the second THEN, then “nested promises” exist:

new Promise((resolve) = > {
  resolve();
})
  .then(() = > {
    // Do a bunch of logic
    return new Promise((resolve) = > {
      // Do a bunch of logic
      setTimeout(() = > {
        resolve("final");
      }, 1000);
    });
  })
  .then((pA) = > {
    pA.then(() = > {
      return new Promise((resolve) = > {
        // Do a bunch of logic
        setTimeout(() = > {
          resolve("final");
        }, 1000);
      });
    }).then((pA2) = > {
      pA2.then(() = > {
        return new Promise((resolve) = > {
          // Do a bunch of logic
          setTimeout(() = > {
            resolve("final");
          }, 1000);
        });
      }).then(pA3= > {
        pA3.then(() = > {
          return new Promise((resolve) = > {
            // Do a bunch of logic
            setTimeout(() = > {
              resolve("final");
            }, 1000);
          });
      })
    }).catch(() = > {
      console.log('error');
    })
  })
  .catch(() = > {
    console.log('error');
  });


Copy the code

Suppose we needed to read the files in turn, each relying on the contents of the previous file, then the chain call of Promise would turn into the dreaded “death callback” :

readFile('./file1.txt')
  .then((val) = > {
    return readFile(val);
  })
  .then(promise= > {
    promise.then(val= > {
      return readFile(val);
    }).then(promise= > {
      promise.then(val= > {
        return readFile(val);
      }).then(promise= > {
        promise.then(val= > {
          console.log('It won't work... ');
        });
      });
    });
  });

Copy the code

This actually seems unnecessary, because if we add exception handling logic, it’s not any different from the old death nesting. ResolvePromise solves this problem.

Implement the THEN method and return a Promise called promiseA. If the onFulfilled method or the onFulfilled method returns a Promise called promiseB, then the state of the Promise depends on the Promise. That is, if the state of promiseB is fulfilled, then the state of promiseA is fullfiled. If the state of promiseB is rejected, then the state of promiseA is also rejected. Also, the final value of the promiseB is the final value of the promiseA.

Simply put: State control of the promiseA is transferred to the promiseB. The process in the promiseB is “deconstructed” by the resolvePromise. Assuming the final value is Y and pX is Promise, then:

Y = resolvePromise(pA(pB(pC(pD(pE(Y))))))
Copy the code

In this case, the above example could become:

readFile('./file1.txt')
  .then(val= > readFile(val))
  .then(val= > readFile(val))
  .then(val= > readFile(val))
  .then(val= > readFile(val))
  .then(val= > readFile(val))
  .then(val= > readFile(val))
  .then(val= > readFile(val))
Copy the code

At a glance how clear! Ok, with that said, let’s implement the resolvePromise.

implementationresolvePromise

The resolvePromise method takes four arguments: Promise, X, resolve, reject, where PROMISE is the promise instance returned by then method, resolve and Reject are the methods of the promise instance, and X is the return value of implementing onFulfilled or onRejected.

ResolvePromise mainly deals with the following three points:

  1. Disable circular calls
  2. If x is a Promise instance
  3. If x is a Thenable object

First, disable circular calls, which determine if promise and X are the same object:

const resolvePromise = (promise, x, resolve, reject) = > {
  // Disable circular calls
  if (promise === x) {
    reject(new TypeError("Forbid loop calls")); }};Copy the code

Then it is to determine whether it is a Promise instance. If it is a Promise instance and it is in the Pending state, then wait for the state of X to change and get the value Y after the Promise execution. Because y can also be a Thenable object or a Promise instance, a resolvePromise call is required for y. If the Promise state is fulfilled or Rejected, then x. teng can be called directly.

const resolvePromise = (promise, x, resolve, reject) = > {
  // Disable circular calls
  if (promise === x) {
    reject(new TypeError("Forbid loop calls"));
  }

  // If x is a Promise instance
  if (x instanceof MyPromise) {
    // If x is Pending, then resolve will be called until X is Fulfilled or Rejected
    if (x.status === PromiseStatus.Pending) {
      x.then(
        (y) = > {
          // Further resolvePromise because y may also be a Promise instance/thenable object
          resolvePromise(promise, y, resolve, reject);
        },
        (r) = >{ reject(r); }); }else{ x.then(resolve, reject); }}};Copy the code

We then proceed to determine whether x is a Thenable object. If so, we call this of the x. Chen method with x as the value y. As with previous promises, y may be either a Promise instance or a Thenable object. Therefore, resolvePromise processing is also required. Resolve (x) if x is not a Thenable object.

The main thing to note is that the Thenable object is handled in a similar way to the Promise object, but because the state of the Promise instance will never be executed again if any state changes, there is no need to worry about repeated execution. However, the thenable object does not have this limitation, so you need to add an auxiliary variable to determine whether x.teng has been executed. If it has been executed, it is ignored.

const resolvePromise = (promise, x, resolve, reject) = > {
  // Disable circular calls
  if (promise === x) {
    reject(new TypeError("Forbid loop calls"));
  }

  // If x is a Promise instance
  if (x instanceof MyPromise) {
    // If x is Pending, then resolve will be called until X is Fulfilled or Rejected
    if (x.status === PromiseStatus.Pending) {
      x.then(
        (y) = > {
          // Further resolvePromise because y may also be a Promise instance/thenable object
          resolvePromise(promise, y, resolve, reject);
        },
        (r) = >{ reject(r); }); }else{ x.then(resolve, reject); }}else {
    // x is not a Promise instance

    // x is a function or object
    if (typeof x === "function"|| (x ! = =null && typeof x === "object")) {
      let called = false;
      try {
        const then = x.then;
        // Make sure to execute only once
        if (typeof then === "function") {
          then.call(
            x,
            (y) = > {
              if (called) {
                return;
              }
              called = true;
              resolvePromise(promise, y, resolve, reject);
            },
            (r) = > {
              if (called) {
                return;
              }
              called = true; reject(r); }); }else {
          // Is an object/function, but not thenableresolve(x); }}catch (error) {
        if (called) {
          return;
        }
        called = true; reject(error); }}else {
      // Resolve instead of Promise or thenableresolve(x); }}};Copy the code

At this point, the full resolvePromise is executed.

Final: the Final implementation, and passed the test

By engaging Step1, Step2 and Step3, the final Promise code is as follows:

const PromiseStatus = {
  Pending: "PENDING".FulFilled: "FULFILLED".Rejected: "REJECTED"
};

class MyPromise {
  constructor(excutor) {
    this.status = PromiseStatus.Pending;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];
    this.value = undefined;
    this.reason = undefined;

    // When the promise is successfully implemented, all onFulfilled shall be called back successively according to its registration order
    const resolve = (value) = > {
      // The state of a promise can only be changed once
      if (this.status === PromiseStatus.Pending) {
        setTimeout(() = > {
          this.status = PromiseStatus.FulFilled;
          this.value = value;
          this.onFulfilledCallbacks.forEach((callback) = >{ callback(value); }); }); }};// When a promise is rejected, all onRejected are called back in the order in which they were registered
    const reject = (reason) = > {
      // The state of a promise can only be changed once
      if (this.status === PromiseStatus.Pending) {
        setTimeout(() = > {
          this.status = PromiseStatus.Rejected;
          this.reason = reason;
          this.onRejectedCallbacks.forEach((callback) = >{ callback(reason); }); }); }};try {
      excutor(resolve, reject);
    } catch(error) { reject(error); }}then(onFulfilled, onRejected) {
    onFulfilled =
      typeof onFulfilled === "function" ? onFulfilled : (value) = > value;
    onRejected =
      typeof onRejected === "function"
        ? onRejected
        : (reason) = > {
            throw reason;
          };

    let promise2;
    let _resolve;
    let _reject;

    const resolvePromise = (promise, x, resolve, reject) = > {
      // Disable circular calls
      if (promise === x) {
        reject(new TypeError("Forbid loop calls"));
      }

      // If x is a Promise instance
      if (x instanceof MyPromise) {
        // If x is Pending, then resolve will be called until X is Fulfilled or Rejected
        if (x.status === PromiseStatus.Pending) {
          x.then(
            (y) = > {
              // Further resolvePromise because y may also be a Promise instance/thenable object
              resolvePromise(promise, y, resolve, reject);
            },
            (r) = >{ reject(r); }); }else{ x.then(resolve, reject); }}else {
        // x is not a Promise instance

        // x is a function or object
        if (typeof x === "function"|| (x ! = =null && typeof x === "object")) {
          let called = false;
          try {
            const then = x.then;
            // Make sure to execute only once
            if (typeof then === "function") {
              then.call(
                x,
                (y) = > {
                  if (called) {
                    return;
                  }
                  called = true;
                  resolvePromise(promise, y, resolve, reject);
                },
                (r) = > {
                  if (called) {
                    return;
                  }
                  called = true; reject(r); }); }else{ resolve(x); }}catch (error) {
            if (called) {
              return;
            }
            called = true; reject(error); }}else{ resolve(x); }}};const onFulfilledCallback = (value) = > {
      try {
        const x = onFulfilled(value);
        resolvePromise(promise2, x, _resolve, _reject);
      } catch(error) { _reject(error); }};const onRejectedCallback = (value) = > {
      try {
        const x = onRejected(value);
        resolvePromise(promise2, x, _resolve, _reject);
      } catch(error) { _reject(error); }}; promise2 =new MyPromise((resolve, reject) = > {
      _resolve = resolve;
      _reject = reject;

      if (this.status === PromiseStatus.Pending) {
        this.onFulfilledCallbacks.push(onFulfilledCallback);
        this.onRejectedCallbacks.push(onRejectedCallback);
      }

      if (this.status === PromiseStatus.FulFilled) {
        setTimeout(() = > onFulfilledCallback(this.value));
      }

      if (this.status === PromiseStatus.Rejected) {
        setTimeout(() = > onRejectedCallback(this.reason)); }});returnpromise2; }}Copy the code

At last, the promises- aplus-Tests tool was used to test the promises. The results showed that 872 use cases were passed, which met the Promise A+ specification!