Recently, I read the Promise/A+ specification and tried to implement A Promise class that meets promises- Aplus-Tests. During the process of implementing the specification, I have deepened my understanding of Promise itself. This article will share my implementation process.

  • The code repository for this article is here, welcome Star ~.

Front knowledge

  1. Promises are a solution to asynchronous problems and serve as placeholders for asynchronous operations.
  2. Each Promise has only three states:pending.fulfilledrejected, the state can only be frompendingTransferred to thefulfilledorrejectedOnce the state becomesfulfilledorrejectedCan no longer change its state.

2.1.1 When pending, a promise: 2.1.1.1 May transition to either the pity or rejected state. 2.1.2 When this is a pity, a promise: 2.1.2.1 Must not transition to any other state. 2.1.2.2 must have a value, which must not change. 2.1.3 When Rejected, 2.1.3.1 must not transition to any other state. 2.1.3.2 must have a reason, which must not change.

  1. thenableObjects are of a class withthenThe object or function of a method.

“Thenable” is an object or function that defines a then method.

  1. Each Promise has one inside itvalueValue, thevalueThe value can be any valid JavaScript data type.

1.3 “value” is any legal JavaScript value (including undefined, a thenable, or a promise).

  1. In addition tovalueProperty, there’s another one inside PromisereasonProperty to store when the Promise state changesrejectedThe reason why

1.5 “reason” is a value that indicates why a promise was rejected.

Tectonic MyPromise class

From the introduction above, we can construct a MyPromise class preliminarily:

class MyPromise {
  constructor(exector) {
    this.status = MyPromise.PENDING;
    this.value = null;
    this.reason = null;
    this.initBind();
    this.init(exector);
  }
  initBind() {// Bind this // Because resolve and reject are executed in exector scope, bind this to the current instance this.resolve = this.resolve.bind(this); this.reject = this.reject.bind(this); } init(exector) { try { exector(this.resolve, this.reject); } catch (err) { this.reject(err); } } resolve(value) {if (this.status === MyPromise.PENDING) {
      this.status = MyPromise.FULFILLED;
      this.value = value;
    }
  }
  reject(reason) {
    if(this.status === MyPromise.PENDING) { this.status = MyPromise.REJECTED; this.reason = reason; }}} // 2.1 A promise must bein one of three states: pending, fulfilled, or rejected.
MyPromise.PENDING = "pending"
MyPromise.FULFILLED = "fulfilled"
MyPromise.REJECTED = "rejected"
Copy the code

Exector is the parameter passed to the constructor when the Promise object is created. The resolve and Reject methods are used to convert the state of the Promise object from pending to pity and Rejected, respectively. And write the corresponding value or Reason value to the Promise object. Now we can run some tests on the above code:

const p1 = new MyPromise((resolve,reject) => {
  const rand = Math.random();
  ifRand (> 0.5) resolve (rand)else reject(rand)
})
console.log(p1)
// MyPromise {status: "fulfilled", value: 0.9121690746412516, reason: null, resolve: ƒ, reject: ƒ}
Copy the code

The above code already allows Promise objects to do state transitions and store value or Reason values, but it’s not enough to simply do state transitions and store values. As an asynchronous solution, we need Promise objects to do something after state transitions. This requires us to provide another THEN method for the Promise object.

A promise must provide a then method to access its current or eventual value or reason.

Then method

The THEN method accepts two parameters: the Promise state transitions to a callback which is fulfilled (successful callback) and the state transitions to a callback which is Rejected (failed callback). These two callback functions are optional.

Accepts two arguments for A Promise’s then Method: Then (onFulfilled, onRejected) 2.2.1 Both onFulfilled and onRejected are optional arguments: This is a big pity If onRejected is not a function, it must be ignored. 2.2.1.2 If onRejected is not a function, it must be ignored.

Add a then method to the MyPromise class:

.then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : () => {}
    onRejected = typeof onRejected === "function" ? onRejected : () => {}
    if (this.status === MyPromise.FULFILLED) {
      try{
        onFulfilled(this.value)
      }catch(e){
        onRejected(e)
      }
    }

    if (this.status === MyPromise.REJECTED) {
      try{
        onRejected(this.reason);
      }catch(e){
        onRejected(e)
      }
    }
  }
...
Copy the code

Let’s test the then method:

const p1 = new MyPromise((resolve) => resolve("Success"))
p1.then(data => console.log(data))
// Success
Copy the code

Here, we preliminarily complete the then method of the MyPromise class. But a closer look at the implementation of the THEN method and MyPromise class above reveals several flaws:

  1. Only state is processedfulfilledrejectedThe case is not processed aspendingIn the case
  2. onFulfilledonRejectedMethods are executed synchronously, that is, calledthenMethod is executedonFulfilledonRejectedmethods
  3. MyPromise in the classresolverejectMethods are also synchronous, which means the following happens:
console.log("START")
const p2 = new MyPromise(resolve => resolve("RESOLVED"))
console.log(p2.value)
console.log("END")
Copy the code

The output is:

START
RESOLVED
END
Copy the code

By specification, promises are supposed to be asynchronous.

2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

The specification also states that you should use macro tasks like setTimeout or setImmediate, or microtasks like MutationObserver or Process.nexttick, To call the onFulfilled and onRejected methods.

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, Or with a “micro-task” mechanism such as MutationObserver or process.nexttick. Since the promise implementation is considered platform code, It may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

  1. The state of the MyPromise object cannot be changed asynchronously, in other words, cannot be satisfiedexectorThe method is asynchronous:
const p3 = new MyPromise(resolve => setTimeout(() => resolve("RESOLVED"))); P3. then(data => console.log(data)) // No output is displayedCopy the code

The reason why there is no output is that when implementing the THEN method, there is no processing of the pending state. Therefore, there is no response to the call of the THEN method in the pending state. Therefore, processing of the pending state is also important in the THEN method. The following is for the above problems, do some improvement.

To improve the

First, you should make sure that the onFulfilled and onRejected methods, as well as resolve and Reject methods, are called asynchronously:

. resolve(value) {if (this.status === MyPromise.PENDING) {
      setTimeout(() => {
        this.status = MyPromise.FULFILLED;
        this.value = value;
        this.onFulfilledCallback.forEach(cb => cb(this.value));
      })
    }
  }

  reject(reason) {
    if (this.status === MyPromise.PENDING) {
      setTimeout(() => { this.status = MyPromise.REJECTED; this.reason = reason; this.onRejectedCallback.forEach(cb => cb(this.reason)); }}})then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : () => {}
    onRejected = typeof onRejected === "function" ? onRejected : () => {}
    if (this.status === MyPromise.FULFILLED) {
      setTimeout(() => {      
        try{
          onFulfilled(this.value)
        }catch(e){
          onRejected(e)
        }
      })
    }

    if (this.status === MyPromise.REJECTED) {
      setTimeout(() => {      
        try{
          onRejected(this.reason);
        }catch(e){
          onRejected(e)
        }
      })
    }
  }
...
Copy the code

Next, you need to set up two queues in the MyPromise class: onAborledCallback and onRejectedCallback to store the incoming callback when calling the THEN method in the pending state. When the Resolve and Reject methods are called, the callbacks stored in the queue are called in sequence (doesn’t that feel like a browser’s event loop mechanism).

class MyPromise { constructor(exector) { this.status = MyPromise.PENDING; this.value = null; this.reason = null; / * * * 2.2.6then may be called multiple timesOn the same promise * 2.2.6.1 If/when promise is very depressing, all respective oncallbacks must executein the order of their originating calls to then* 2.2.6.2 If/when promise is rejected, all rejected callbacks must executein the order of their originating calls to then.
     */

    this.onFulfilledCallback = [];
    this.onRejectedCallback = [];
    this.initBind();
    this.init(exector);
  }
  initBind() {// Bind this // Because resolve and reject are executed in exector scope, bind this to the current instance this.resolve = this.resolve.bind(this); this.reject = this.reject.bind(this); } init(exector) { try { exector(this.resolve, this.reject); } catch (err) { this.reject(err); } } resolve(value) {if (this.status === MyPromise.PENDING) {
      setTimeout(() => {
        this.status = MyPromise.FULFILLED;
        this.value = value;
        this.onFulfilledCallback.forEach(cb => cb(this.value));
      })
    }
  }

  reject(reason) {
    if (this.status === MyPromise.PENDING) {
      setTimeout(() => { this.status = MyPromise.REJECTED; this.reason = reason; this.onRejectedCallback.forEach(cb => cb(this.reason)); }}})then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : () => {}
    onRejected = typeof onRejected === "function" ? onRejected : () => {}
    if (this.status === MyPromise.FULFILLED) {
      setTimeout(() => {      
        try{
          onFulfilled(this.value)
        }catch(e){
          onRejected(e)
        }
      })
    }

    if (this.status === MyPromise.REJECTED) {
      setTimeout(() => {      
        try{
          onRejected(this.reason);
        }catch(e){
          onRejected(e)
        }
      })
    }

    if(this. Status = = = MyPromise. PENDING) {/ / to load in the onFulfilled and onRejected function enclosing onFulfilledCallback. Push ((value) = > { try{ onFulfilled(value) }catch(e){ onRejected(e) } }) this.onRejectedCallback.push((reason) => { try{ onRejected(reason) }catch(e){onRejected(e)}})}} // 2.1a promise must bein one of three states: pending, fulfilled, or rejected.
MyPromise.PENDING = "pending"
MyPromise.FULFILLED = "fulfilled"
MyPromise.REJECTED = "rejected"
Copy the code

Run some tests:

console.log("===START===")
const p4 = new MyPromise(resolve => setTimeout(() => resolve("RESOLVED")));
p4.then(data => console.log(1,data))
p4.then(data => console.log(2,data))
p4.then(data => console.log(3,data))
console.log("===END===")
Copy the code

Output result:

===START===
===END===
1 'RESOLVED'
2 'RESOLVED'
3 'RESOLVED'
Copy the code

Implement chain calls

The specification also states that the THEN method must return a new Promise object to implement the chain call.

Promise1 = promise1. Then (onFulfilled, onRejected);

If ondepressing and onRejected are functions, use the return value of the function call to change the state of the newly returned promise2 object.

2.2.7.1 If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x). 2.2.7.2 If either onFulfilled or onRejected throws an exception epromise2 must be rejected with e as the reason.

The Promise Resolution Procedure mentioned here is to process the promise2 state uniformly according to the different return values of onFulfilled and onRejected methods. We will ignore the Promise Resolution Procedure for now and provide the implementation later.

In addition, if onFulfilled and onRejected are not functions, then the state of promise1 will be changed according to the current state of the promise object (promise1).

2.2.7.3 This is a big pity If onprogressively is not a function and promise1 is a big pity. This is a big pity with the same value as promise1. 2.2.7.4 If onRejected is not a function and promise1 is a pity rejected, promise2 must be rejected with the same reason as promise1.

The onFulfilled and onRejected functions have been processed in the previous code. If they are not functions, provide a default value:

onFulfilled = typeof onFulfilled === "function" ? onFulfilled : () => {}
onRejected = typeof onRejected === "function" ? onRejected : () => {}
Copy the code

And each time when the onFulfilled or onRejected method is called, the value or reason attribute of the current instance will be passed. Therefore, in the special case where onFulfilled or onRejected is not a function, the parameters passed to them can be returned directly. Promise2 still uses the return value of onFulfilled or onRejected to change the state:

onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason }
Copy the code

The above solution also incidentally solves the value penetration problem. Value penetration means that when the THEN method is called, if no parameter is passed in, the THEN method in the lower chain can still obtain value or Reason normally.

new MyPromise(resolve => setTimeout(() => { resolve("Success") }))
.then()
.then()
.then()
...
.then(data => console.log(data));
Copy the code

The then method can be modified as follows:

...then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
    onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason }
    let promise2;
    if (this.status === MyPromise.FULFILLED) {
      return promise2 = new MyPromise((resolve,reject) => {
        setTimeout(() => {      
          try{
            const x = onFulfilled(this.value)
            resolve(x);
          }catch(e){
            reject(e)
          }
        })
      })
    }

    if (this.status === MyPromise.REJECTED) {
      return promise2 = new MyPromise((resolve,reject) => {
        setTimeout(() => {      
          try{
            const x = onRejected(this.reason)
            resolve(x);
          }catch(e){
            reject(e)
          }
        })
      })
    }

    if (this.status === MyPromise.PENDING) {
      returnPromise2 = new MyPromise((resolve,reject) => {// This is a pity and onRejected function this.onFulfilledCallback.push((value) => { try{ const x = onFulfilled(value) resolve(x) }catch(e){ reject(e) } }) this.onRejectedCallback.push((reason) => { try{ const x = onRejected(reason) resolve(x); }catch(e){reject(e)}})}} ··· ·Copy the code

The specification states that the THEN method must return a new Promise object (promisE2), and that the state of the new promisE2 must depend on the state of the Promise object (promise1) that calls the THEN method. The promise2 state can be changed only after the promise1 state becomes a pity or Rejected. Thus, in the implementation of the THEN method, a method that changes the state of the Promise object (promisE1) is queued to the callback function when the current state of the Promise object (promisE1) is pending.

Implement the resolvePromise method

The code above deals with the return value of the onFulfilled and onRejected methods, as well as the chain call of the THEN method. Now consider this question: what if the onFulfilled or onRejected method returns a Promise object, or another object with the THEN method (Thenable object)? As mentioned in the specification, a Promise Resolution Procedure method is provided for uniform processing of the onFulfilled or onRejected return values, so as to adapt to different return value types.

2.2.7.1 If either ondepressing or onRejected returns a value X, run the Promise Resolution Procedure [[Resolve]](promise2, x).

We named this method the resolvePromise method and designed it as a static method on the MyPromise class. This is a big pity. The static method () will change the state of the Promise object returned by the then method according to the different return value (x) of onFulfilled or onRejected. We have moved the process of changing the state of the promise2 object to the resolvePromise method for more detail. Here’s an implementation of the resolvePromise method:

MyPromise.resolvePromise = (promise2,x,resolve,reject) => {
  let called = false;
  /**
   * 2.3.1 If promise and x refer to the same object, reject promise with a TypeError as the reason.
   */
  if(promise2 === x){
    return reject(new TypeError("cannot return the same promise object from onfulfilled or on rejected callback."))}if(x instanceof MyPromise){** * new MyPromise(resolve => {* resolve("Success")
     * }).then(data => {
     *  return new MyPromise(resolve => {
     *    resolve("Success2"*}) *}) */if(x.status === mypromise.pending){/** * 2.3.2.1 If x is PENDING, The promise must remain pending until X is a big pity or rejected. */ x.teng (y => {// This is a big pity. To set the state of promise2 // The gaze above shows the return of the Promise object, called herethen// Get the value of x by y or reason // Use the value of y to change the state of promise2 // As in the example above, the generated Promise object, /** * new Promise(resolve => {* resolve() /** * new Promise(resolve => {* resolve())"Success")
         * }).then(data => {
         *  return new Promise(resolve => {
         *    resolve(new Promise(resolve => {
         *      resolve("Success3") * * *}})}))). Then (data = > console. The log (data)) * / / / in a word, use"return"State of the last Promise object in the chain, Mypromise. resolvePromise(promise2, y, resolve, reject)},reason => {reject(reason)})}else{/** * 2.3 If x is a thenable, it attempts to make the promise to adopt the state of x, Less time * * Less time * * Less time * * less time * * less time * * less time * * less time Adopt its state [3.4] : This is a big pity If/when x is fulfilled, fulfill promise with the same value. Reject promise with the same reason. */ x. hen(resolve,reject)} /** * 2.3.3 Otherwise,if x is an object or function* /},else if((x ! == null && typeof x ==="object") || typeof x === "function"){/** * 2.3.3.1 Letthen* 2.3.3.2 If retrieving the property x. hen resultsin a thrown exception e, reject promise with e as the reason.
     */
    try{
      // thenMethod may set access limits (setters), so error catching is done here to handle constthen = x.then;
      if(typeof then= = ="function"){/** * 2.3.3.2 If retrieving the property x. hen resultsina thrown exception e, */ /** * 2.3.3.3.1 If/when resolvePromise is called with a value y, Run [[Resolve]](promise, y). * 2.3.3.3.2 If/when rejectPromise is called with a reason r, reject promise with r. */ then.call(x,y => { /** * If both resolvePromise and rejectPromise are called, * or multiple calls to the same argument are made, * the first call takes precedence, and any further calls are ignored. */if(called) return;
          called = true;
          MyPromise.resolvePromise(promise2, y, resolve, reject)          
        },r => {
          if(called) return;
          called = true; reject(r); })}else{resolve (x)}} the catch (e) {/ * * * 2.3.3.3.4 If callingthenThrows an exception e, * 2.3.3.3.4.1 If resolvePromise or rejectPromise have been called, * 2.3.3.3.4.2 Otherwise, reject promise with eas the reason. */if(called) return;
      called = true;
      reject(e)
    }
  }else{
    // If x is not an object or function, fulfill promise with x. resolve(x); }}Copy the code

The resolvePromise is the most difficult to understand in my implementation of the specification, mainly in the return chain, because I can’t think of specific scenarios. I wrote the specific scene in the above code by way of comments, also confused children can see.

Promises – aplus-Tests

Promises – aplus-Tests allows us to test whether the Promise classes we implement meet the Promise/A+ specification. Promises aplus-Tests need to provide a deferred hook for Promises aplus-tests:

MyPromise.deferred  = function() {
  const defer = {}
  defer.promise = new MyPromise((resolve, reject) => {
    defer.resolve = resolve
    defer.reject = reject
  })
  return defer
}

try {
  module.exports = MyPromise
} catch (e) {
}
Copy the code

Install and run tests:

npm install promises-aplus-tests -D
npx promises-aplus-tests promise.js
Copy the code

The test results are as follows:

here

To the end.