Implement a promises / A+

Time:2021-9-27

Warehouse address of complete high frequency question bank:https://github.com/hzfe/aweso…

Complete high frequency question bank reading address:https://febook.hzfe.org/

This is a coding problem with mature industry specifications. The pre knowledge to complete this problem is to understand what isPromises/A+

The difficulty of this problem is that it has norms. Any solution that does not meet all normative conditions is wrong. At the same time, mature specifications also support mature standardstest case, 872 test cases are officially provided to test all the conditions in the specification one by one. Even if only one fails, it is a wrong answer.

The key to this question is precisely because it has norms. As long as we know the norms well, it is natural to write code. Because the official specification provides all the conditions that a promise meeting the promises / A + specification should have, andRequirementsIn the section, the structure is clear and the logic is fully expressed. We only need to convert the words in the specification into code to realize the promise of a promises / A + specification.

Write code

Because there are many rules and regulations, we disassemble them into three parts to understand memory: basic framework, then method and promise handler.

Each block consists of two parts:

  • Flow chart: it shows the key steps of code logic, and it is also the priority point to understand and remember.
  • Implementation code: it shows the specific details of code logic and is the perfection of key steps.

Among them,The code number will be indicated at the points related to the code regulations

Again,The key to this question is to be familiar with the standard! Sharpen the knife without mistaking the woodcutter. Be sure to be familiar with it first! Familiar! Familiar!

1. Basic framework

1.1 flow chart

Implement a promises / A+

1.2 implementation code

function Promise(executor) {
  //   2.1.   Promise   State of
  //Promise must be in one of the following three states: pending, fully or rejected.
  this.state = "pending";
  //2.2.6.1. If promise is in the full state, all corresponding onfulfilled callbacks must be executed according to the original calling sequence of their corresponding then.
  this.onFulfilledCallback = [];
  //2.2.6.2. If promise is in the rejected state, all corresponding onrejected callbacks must be executed according to the original calling sequence of their corresponding then.
  this.onRejectedCallback = [];

  const self = this;

  function resolve(value) {
    setTimeout(function () {
      //2.1.1. When promise is in pending status:
      //2.1.1.1. It can be switched to the fully or rejected state.
      //2.1.2. When promise is in full status:
      //2.1.2.1. Do not transition to any other state.
      //2.1.2.2. There must be a value that cannot be changed.
      if (self.state === "pending") {
        self.state = "fulfilled";
        self.data = value;
        //2.2.6.1. If promise is in the full state, all corresponding onfulfilled callbacks must be executed according to the original calling sequence of their corresponding then.
        for (let i = 0; i < self.onFulfilledCallback.length; i++) {
          self.onFulfilledCallback[i](value);
        }
      }
    });
  }

  function reject(reason) {
    setTimeout(function () {
      //2.1.1. When promise is in pending status:
      //2.1.1.1. It can be switched to the fully or rejected state.
      //2.1.3. When promise is in rejected status:
      //2.1.2.1. Do not transition to any other state.
      //2.1.2.2. There must be a value that cannot be changed.
      if (self.state === "pending") {
        self.state = "rejected";
        self.data = reason;
        //2.2.6.2. If promise is in the rejected state, all corresponding onrejected callbacks must be executed according to the original calling sequence of their corresponding then.
        for (let i = 0; i < self.onRejectedCallback.length; i++) {
          self.onRejectedCallback[i](reason);
        }
      }
    });
  }

  //   Supplementary note: the function passed in by the user may also execute exceptions, so try... Catch is used here
  try {
    executor(resolve, reject);
  } catch (reason) {
    reject(reason);
  }
}

2. Then method

2.1 flow chart

Implement a promises / A+

2.2 implementation code

//   2.2.   then   method
//   A promise must provide a then method to access its current or final value or the reason for rejected.
//   The then method of a promise accepts two parameters:
// promise.then(onFulfilled, onRejected)
Promise.prototype.then = function (onFulfilled, onRejected) {
  const self = this;

  let promise2;
  //   2.2.7.   then   One must be returned   promise
  return (promise2 = new Promise(function (resolve, reject) {
    //   2.2.2.   If   onFulfilled   Is a function:
    //2.2.2.1. It must be called after the state of promise changes to fully, and take the value of promise as its first parameter.
    //2.2.2.2. It must not be called before the state of promise becomes fully.
    //2.2.2.3. It can only be called once at most.
    if (self.state === "fulfilled") {
      //2.2.4. Onfulfilled or onrejected cannot be called before the execution context stack contains only platform code.
      //3.1. This can be achieved through "macro task" mechanism (such as setTimeout or setimmediate) or "micro task" mechanism (such as mutationobserver or process.nexttick).
      setTimeout(function () {
        //2.2.1. Onfulfilled and onrejected are optional parameters:
        //2.2.1.1. If onfulfilled is not a function, it must be ignored.
        if (typeof onFulfilled === "function") {
          try {
            //2.2.2.1. It must be called after the state of promise changes to fully, and take the value of promise as its first parameter.
            //2.2.5. Onfulfilled and onrejected must be called as functions.
            const x = onFulfilled(self.data);
            //2.2.7.1. If onfulfilled or onrejected returns a value of X, run the promise handler   [[Resolve]](promise2, x)。
            promiseResolutionProcedure(promise2, x, resolve, reject);
          } catch (e) {
            //2.2.7.2. If onfulfilled or onrejected throws an exception, promise 2 must use e as reason to change to the rejected state.
            reject(e);
          }
        } else {
          //2.2.7.3. If onfulfilled is not a function and promise1 is in the completed state, PROMISE2 must change to the completed state with the same value as promise1.
          resolve(self.data);
        }
      });
    }
    //   2.2.3.   If   onRejected   Is a function,
    //2.2.3.1. It must be called after the state of promise changes to rejected, and take the reason of promise as its first parameter.
    //2.2.3.2. It must not be called before the state of promise changes to rejected.
    //2.2.3.3. It can only be called once at most.
    else if (self.state === "rejected") {
      //2.2.4. Onfulfilled or onrejected cannot be called before the execution context stack contains only platform code.
      //3.1. This can be achieved through "macro task" mechanism (such as setTimeout or setimmediate) or "micro task" mechanism (such as mutationobserver or process.nexttick).
      setTimeout(function () {
        //2.2.1. Onfulfilled and onrejected are optional parameters:
        //2.2.1.2. If onrejected is not a function, it must be ignored.
        if (typeof onRejected === "function") {
          try {
            //2.2.3.1. It must be called after the state of promise changes to rejected, and take the reason of promise as its first parameter.
            //2.2.5. Onfulfilled and onrejected must be called as functions.
            const x = onRejected(self.data);
            //2.2.7.1. If onfulfilled or onrejected returns a value of X, run the promise handler   [[Resolve]](promise2, x)。
            promiseResolutionProcedure(promise2, x, resolve, reject);
          } catch (e) {
            //2.2.7.2. If onfulfilled or onrejected throws an exception, promise 2 must use e as reason to change to the rejected state.
            reject(e);
          }
        }
        //2.2.7.4. If onrejected is not a function and promise1 is in the rejected state, PROMISE2 must change to the rejected state with the same reason as promise1.
        else {
          reject(self.data);
        }
      });
    } else if (self.state === "pending") {
      //2.2.6. Then may be called multiple times by the same promise.

      //2.2.6.1. If promise is in the full state, all corresponding onfulfilled callbacks must be executed according to the original calling sequence of their corresponding then.
      self.onFulfilledCallback.push(function (promise1Value) {
        if (typeof onFulfilled === "function") {
          try {
            //2.2.2.1. It must be called after the state of promise changes to fully, and take the value of promise as its first parameter.
            //2.2.5. Onfulfilled and onrejected must be called as functions.
            const x = onFulfilled(self.data);
            //2.2.7.1. If onfulfilled or onrejected returns a value of X, run the promise handler   [[Resolve]](promise2, x)。
            promiseResolutionProcedure(promise2, x, resolve, reject);
          } catch (e) {
            //2.2.7.2. If onfulfilled or onrejected throws an exception, promise 2 must use e as reason to change to the rejected state.
            reject(e);
          }
        }
        //2.2.7.3. If onfulfilled is not a function and promise1 is in the completed state, PROMISE2 must change to the completed state with the same value as promise1.
        else {
          resolve(promise1Value);
        }
      });
      //2.2.6.2. If promise is in the rejected state, all corresponding onrejected callbacks must be executed according to the original calling sequence of their corresponding then.
      self.onRejectedCallback.push(function (promise1Reason) {
        if (typeof onRejected === "function") {
          try {
            //2.2.3.1. It must be called after the state of promise changes to rejected, and take the reason of promise as its first parameter.
            //2.2.5. Onfulfilled and onrejected must be called as functions.
            const x = onRejected(self.data);
            //2.2.7.1. If onfulfilled or onrejected returns a value of X, run the promise handler   [[Resolve]](promise2, x)。
            promiseResolutionProcedure(promise2, x, resolve, reject);
          } catch (e) {
            //2.2.7.2. If onfulfilled or onrejected throws an exception, promise 2 must use e as reason to change to the rejected state.
            reject(e);
          }
        }
        //2.2.7.4. If onrejected is not a function and promise1 is in the rejected state, PROMISE2 must change to the rejected state with the same reason as promise1.
        else {
          reject(promise1Reason);
        }
      });
    }
  }));
};

3. Promise handler

3.1 flow chart

Implement a promises / A+

3.2 implementation code

//   2.3.   Promise   processing program
//Promise handler is an abstract operation that takes promise and value as inputs, which we express as   [[Resolve]](promise, x)。
//   Supplementary note: here, we will also pass in resolve and reject, because we will perform full or reject operations on promise according to different logic.
function promiseResolutionProcedure(promise2, x, resolve, reject) {
  //2.3.1. If promise and X refer to the same object, promise will reject with a typeerror as the reason.
  if (promise2 === x) {
    return reject(new TypeError("Chaining cycle detected for promise"));
  }

  //2.3.2. If x is a promise, according to its status:
  if (x instanceof Promise) {
    //2.3.2.1. If the status of X is pending, promise must maintain the pending status until the status of X becomes fully or rejected.
    if (x.state === "pending") {
      x.then(function (value) {
        promiseResolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    }
    //2.3.2.2. If the status of X is full, promise performs the full operation with the same value.
    else if (x.state === "fulfilled") {
      resolve(x.data);
    }
    //2.3.2.3. If the status of X is rejected, promise uses the same reason to execute the reject operation.
    else if (x.state === "rejected") {
      reject(x.data);
    }
    return;
  }

  //   2.3.3.   In addition, if   x   Is an object or function,
  if (x && (typeof x === "object" || typeof x === "function")) {
    //2.3.3.3.3. If both resolvepromise and rejectpromise are called, or the same parameters are called multiple times, the first call takes precedence and any subsequent calls will be ignored.
    let isCalled = false;

    try {
      //   2.3.3.1.   Declare a   then   Variable to save   then
      let then = x.then;
      //2.3.3.3. If then is a function, call it with X as this. The first parameter is resolvepromise and the second parameter is rejectproject, where:
      if (typeof then === "function") {
        then.call(
          x,
          //2.3.3.3.1. Suppose resolvepromise is called with a value named y, run the promise handler   [[Resolve]](promise, y)。
          function resolvePromise(y) {
            //2.3.3.3.3. If both resolvepromise and rejectpromise are called, or the same parameters are called multiple times, the first call takes precedence and any subsequent calls will be ignored.
            if (isCalled) return;
            isCalled = true;
            return promiseResolutionProcedure(promise2, y, resolve, reject);
          },
          //2.3.3.3.2. Suppose rejectproject is called with a reason named R, then R is used as the reason to reject the project.
          function rejectPromise(r) {
            //2.3.3.3.3. If both resolvepromise and rejectpromise are called, or the same parameters are called multiple times, the first call takes precedence and any subsequent calls will be ignored.
            if (isCalled) return;
            isCalled = true;
            return reject(r);
          }
        );
      }
      //2.3.3.4. If then is not a function, use X as the value to perform a full operation on promise.
      else {
        resolve(x);
      }
    } catch (e) {
      //2.3.3.2. If the result of retrieving x.then throws exception e, use e as reason to reject promise.
      //   2.3.3.3.4.   If called   then   An exception is thrown when   e,
      //2.3.3.3.4.1. If resolvepromise or rejectpromise has been called, the exception is ignored.
      if (isCalled) return;
      isCalled = true;
      //2.3.3.3.4.2. Otherwise, use e as reason to reject promise.
      reject(e);
    }
  }
  //2.3.4. If x is not an object or function, use X as the value to perform the full operation on promise.
  else {
    resolve(x);
  }
}

4. Complete code

function Promise(executor) {
  this.state = "pending";
  this.onFulfilledCallback = [];
  this.onRejectedCallback = [];

  const self = this;

  function resolve(value) {
    setTimeout(function () {
      if (self.state === "pending") {
        self.state = "fulfilled";
        self.data = value;
        for (let i = 0; i < self.onFulfilledCallback.length; i++) {
          self.onFulfilledCallback[i](value);
        }
      }
    });
  }

  function reject(reason) {
    setTimeout(function () {
      if (self.state === "pending") {
        self.state = "rejected";
        self.data = reason;
        for (let i = 0; i < self.onRejectedCallback.length; i++) {
          self.onRejectedCallback[i](reason);
        }
      }
    });
  }

  try {
    executor(resolve, reject);
  } catch (reason) {
    reject(reason);
  }
}

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

  let promise2;

  return (promise2 = new Promise(function (resolve, reject) {
    if (self.state === "fulfilled") {
      setTimeout(function () {
        if (typeof onFulfilled === "function") {
          try {
            const x = onFulfilled(self.data);

            promiseResolutionProcedure(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        } else {
          resolve(self.data);
        }
      });
    } else if (self.state === "rejected") {
      setTimeout(function () {
        if (typeof onRejected === "function") {
          try {
            const x = onRejected(self.data);

            promiseResolutionProcedure(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        } else {
          reject(self.data);
        }
      });
    } else if (self.state === "pending") {
      self.onFulfilledCallback.push(function (promise1Value) {
        if (typeof onFulfilled === "function") {
          try {
            const x = onFulfilled(self.data);

            promiseResolutionProcedure(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        } else {
          resolve(promise1Value);
        }
      });

      self.onRejectedCallback.push(function (promise1Reason) {
        if (typeof onRejected === "function") {
          try {
            const x = onRejected(self.data);

            promiseResolutionProcedure(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        } else {
          reject(promise1Reason);
        }
      });
    }
  }));
};

function promiseResolutionProcedure(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError("Chaining cycle detected for promise"));
  }

  if (x instanceof Promise) {
    if (x.state === "pending") {
      x.then(function (value) {
        promiseResolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    } else if (x.state === "fulfilled") {
      resolve(x.data);
    } else if (x.state === "rejected") {
      reject(x.data);
    }
    return;
  }

  if (x && (typeof x === "object" || typeof x === "function")) {
    let isCalled = false;

    try {
      let then = x.then;

      if (typeof then === "function") {
        then.call(
          x,
          function resolvePromise(y) {
            if (isCalled) return;
            isCalled = true;
            return promiseResolutionProcedure(promise2, y, resolve, reject);
          },
          function rejectPromise(r) {
            if (isCalled) return;
            isCalled = true;
            return reject(r);
          }
        );
      } else {
        resolve(x);
      }
    } catch (e) {
      if (isCalled) return;
      isCalled = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}

module.exports = Promise;

Test code

At the beginning, we said that the promises / A + specification is equipped with mature test cases. We must pass all of them before we can write the code correctly. Let’s use 872 official test cases to test whether our complete code meets the promises / A + specification.

1. Expose a simple adapter interface

// test.js

//   Import what we wrote   promise
const Promise = require("./promise.js");

//   Expose one according to official documents   deferred   Method to return a containing   promise、resolve、reject   Object of
Promise.deferred = function () {
  const obj = {};

  obj.promise = new Promise(function (resolve, reject) {
    obj.resolve = resolve;
    obj.reject = reject;
  });

  return obj;
};

module.exports = Promise;

2. Run command

$ npx promises-aplus-tests test.js

3. Test results

Implement a promises / A+

Perfect pass!

reference material

  1. Promises/A+
  2. Promises/A+ Compliance Test Suite