The Promise/A+ specification is an open source, robust and interoperable JavaScript Promise specification. Implementing A Promise by reading the PromiseA+ specification documentation will help us better understand the core ideas of promises. Promises represent the end result of asynchronous operations. The main way to interact with promises is through the THEN method, which at its core collects callbacks and then triggers all of the then collected callbacks through reslove or Reject, a publish-subscribe model.

PromiseA+ specifies the following:

1. The term

1.1 A promise is an object or function with a THEN method that conforms to this specification. 1.2 Thenable is an object or function that defines a THEN method. 1.3 Value is any valid JavaScript value (including undefined, thenable, or promise). 1.4 Exception is a value thrown using a throw statement. 1. There is a reason why the promise is rejected.

2. Request

2.1 promise state

A promise must be one of three states: Pending, depressing, or Rejected. 2.1.1 When the Promise is in pending state:

2.1.1.1 You can change to the state of depressing or Rejected.Copy the code

2.1.2 When the promise is fulfilled:

2.1.2.1 It must not be possible to change to any other state. 2.1.2.2 There must be a value and the value must not be changed.Copy the code

2.1.3 When the Promise is in the Rejected State:

2.1.3.1 It must not be possible to change to any other state. 2.1.3.2 There must be a Reason and the value cannot be changed.Copy the code

Here, “must not change” means immutable identity (i.e. ===), but not deep immutability.


According to 2.1 state description information, the following information can be obtained: 1. Each promise has three states: Pennding wait state, resolve becomes successful state, and Reject becomes failed state. Once you succeed, you can’t fail. Once you fail, you can’t succeed. 3. When a PROMISE throws an exception, it also fails. You can write roughly the following code:

// Define the state enumeration
const STATUS = {
  pennding: 'PENNDING'.fulfilled: "FULFILLED".rejected: 'REJECTED'
}

class Promise{
  constructor(executor) { // Executor is a new Promise(resolve,reject)=>{}) passed in (reslove,reject)=>{... } function
    
    this.status = STATUS.pennding // The current state is created pennding
    this.value = null / / the current value
    this.reason = null // Failure cause

    / / resolve function
    const resolve = (value) = > {
      if (this.status === STATUS.pennding) {
        this.status = STATUS.fulfilled // This will be a big pity
        this.value = value
      }
    }
    
    / / reject function
    const reject = (reason) = > {
      if (this.status === STATUS.pennding) {
        this.status = STATUS.rejected // This will be a big pity
        this.reason = reason
      }
    }
    
    // Catch exceptions to handle errors thrown by user reslove/reject, such as throw Error(' XXXXX ')
    try {
      executor(reslove, reject)
    } catch (err) {
      reject(err)
    }
  }
  
  then(onFulfilled, onRejected) {
    // Successful callback
    if (this.status === STATUS.fulfilled) { // The ondepressing method can be implemented only when the state is fulfilled, and the user will change the state to fulfilled when calling resolve
      onFulfilled(this.value)
    }
    // Failed callback
    if (this.status === STATUS.rejected) { The onRejected method can be executed only when the reject state is rejected. When the user calls reject, the state is changed to Rejected
      onRejected(this.reason)
    }
  }
}

// case1
const promise = new Promise((resolve,reject) = >{
  resolve('success')
  reject('error')
})
promise.then(
  value= > console.log(value),
  reason= > console.log(reason),
)
Reject (reject); // => reject (reject
Copy the code

This implements the simplest Promise class that can be called then. However, there are a number of problems. For example, if there is asynchronous code in then, resolve or reject will not be executed because the state is still Pennding. This problem can be solved in a publish-subscribe mode, where incoming callbacks are collected each time the state is pennding, and all collected callbacks are executed in resolve or Reject. It can be implemented as follows:

// Define the state enumeration
const STATUS = {
  pennding: 'PENNDING'.fulfilled: "FULFILLED".rejected: 'REJECTED'
}

class Promise {
  constructor(executor) { // Executor is new Promise((reslove,reject)=>{}) (reslove,reject)=>{... } function

    this.status = STATUS.pennding // The current state is created pennding
    this.value = null / / the current value
    this.reason = null // Failure cause

    this.onReslovedCallbacks = [] // Store the successful callback function queue --> new code
    this.onRejectedCallbacks = [] // Store the failed callback function queue --> add new code

    / / reslove function
    const reslove = (value) = > {
      if (this.status === STATUS.pennding) {
        this.status = STATUS.fulfilled // This will be a big pity
        this.value = value
        // Perform the depressing functions for all subscriptions
        this.onReslovedCallbacks.forEach(fn= > fn()) // --> new code}}/ / reject function
    const reject = (reason) = > {
      if (this.status === STATUS.pennding) {
        this.status = STATUS.rejected // This will be a big pity
        this.reason = reason
        // Execute the Rejected function for all subscriptions
        this.onRejectedCallbacks.forEach(fn= > fn()) // --> new code}}// Catch exceptions to handle errors thrown by user resolve/reject, such as throw Error(' XXXXX ')
    try {
      executor(resolve, reject)
    } catch (err) {
      reject(err)
    }
  }

  then(onFulfilled, onRejected) {
    // Successful callback
    if (this.status === STATUS.fulfilled) { // The ondepressing method can be implemented only when the state is fulfilled, and the user will change the state to fulfilled when calling resolve
      onFulfilled(this.value)
    }
    // Failed callback
    if (this.status === STATUS.rejected) { The onRejected method can be executed only when the reject state is rejected. When the user calls reject, the state is changed to Rejected
      onRejected(this.reason)
    }

    // If the state is waiting, the processing callback function of res,rej is put into the corresponding queue
    if (this.status === STATUS.pennding) {
      // Put into the successful callback queue
      this.onResolvedCallbacks.push(() = > { // slicing: Wrap the passed function in a new function, in which you can write unified additional logic (AOP)
        // Additional logic can be written here
        onFulfilled(this.value)
      })
      // Put into the failed callback queue
      this.onRejectedCallbacks.push(() = > { // slicing: Wrap the passed function in a new function, in which you can write unified additional logic (AOP)
        // Additional logic can be written here
        onRejected(this.reason)
      })
    }
  }
}

// case2
const promise = new Promise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('success')},1000)
  setTimeout(() = > {
    reject('error')},2000)
})

promise.then(
  value= > {
    console.log(value, '1')},err= > {
    console.log(err)
  }
)
// => one second later:
// success1
Copy the code

The basic asynchronous Promise classes have been implemented, but many of the specification’s features are still missing. Scroll down to see what the PromiseA+ specification promises.


2.2 then method

A promise must provide a then method to access the current or final value or Reason. A Promise’s then method takes two arguments:

promise.then(onFulfilled, onRejected)
Copy the code

2.2.1 Ondepressing and onRejected are both optional parameters:

2.2.1.1 If ondepressing is not a function, it must be ignored. 2.2.1.2 If onRejected is not a function, it must be ignored.Copy the code

2.2.2 If ondepressing is a function:

2.2.2.1 When the promise is fulfilled, it must be invoked, and the value of the promise is the first parameter. 2.2.2.2 It must not be called before the promise enters the depressing state. 2.2.2.3 It must not be called more than once.Copy the code

2.2.3 If onRejected is a function:

2.2.3.1 It must be called when the Promise state is Rejected, and the reason of the promise is its first parameter. 2.2.3.2 It must not be called before the Promise enters the Rejected state. 2.2.3.3 It must not be called more than once.Copy the code

2.2.4 onFulfilled or onRejected cannot be called until the execution context stack contains only the platform code. [3.1] onFulfilled and onRejected must be called in the form of a function (i.e. without this value). [3.2] 2.2.6 THEN can be called multiple times in the same promise:

2.2.6.1 When the promise is fulfilled, each onFulfilled callback must be executed in the order of its original invocation. 2.2.6.2 When the Promise is in the Rejected state, the onRejected callbacks must be executed in the order in which they were originally called.Copy the code

2.2.7 Then must return a Promise [3.3]:

promise2 = promise1.then(onFulfilled, onRejected);
Copy the code
2.2.7.1 If onFulfilled or onRejected returns a value x, run the Promise [[Resolve]](promise2, x). 2.2.7.2 If onFulfilled or onRejected throws a surprise E, promise2 must be promised with E as reason. 2.2.7.3 If ondepressing is not a function and promise1 is in the depressing state, promise2 must transition to the depressing state with the same value as promise1. 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.Copy the code

According to the description information of 2.2 THEN method, the following information can be obtained: 1. Each Promise needs to have a THEN method. Two callback functions are passed as parameters onFulfilled, onRejected, which is not mandatory, but will not affect the transmission value of the next THEN. 2. Each call to THEN must return a promise2, which is a new instance of promise. If a new value x is accepted in reslove/ Reject, then the value x is passed as a callback to the next THEN. Consider: To implement such a requirement, the returned promise must be a new instance, because the promise state is irreversible. Use the new promise’s reslove and Reject to process the value returned by the previous promise. This is a pity, onFulfilled function, so you have to package a layer of function yourself and the return value of this function is the value returned by the last promise. The code is as follows:

// Define the state enumeration
const STATUS = {
  pennding: 'PENNDING'.fulfilled: "FULFILLED".rejected: 'REJECTED'
}

class Promise {
  constructor(executor) { // Executor is new Promise((reslove,reject)=>{}) (reslove,reject)=>{... } function

    this.status = STATUS.pennding // The current state is created pennding
    this.value = null / / the current value
    this.reason = null // Failure cause

    this.onResolvedCallbacks = [] // Store the successful callback queue
    this.onRejectedCallbacks = [] // Store the failed callback function queue

    / / resolve function
    const resolve = (value) = > {
      if (this.status === STATUS.pennding) {
        this.status = STATUS.fulfilled // This will be a big pity
        this.value = value
        // Perform the depressing functions for all subscriptions
        this.onResolvedCallbacks.forEach(fn= > fn())
      }
    }

    / / reject function
    const reject = (reason) = > {
      if (this.status === STATUS.pennding) {
        this.status = STATUS.rejected // This will be a big pity
        this.reason = reason
        // Execute the Rejected function for all subscriptions
        this.onRejectedCallbacks.forEach(fn= > fn())
      }
    }

    // Catch exceptions to handle errors thrown by user resolve/reject, such as throw Error(' XXXXX ')
    try {
      executor(resolve, reject)
    } catch (err) {
      reject(err)
    }
  }

  // then (promise); // then (promise)
  then(onFulfilled, onRejected) {
    // Handle onFulfilled, onRejected is empty, and prevent THEN from losing the value returned by the last THEN
    onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : x= > x;
    onRejected = typeof onRejected == 'function' ? onRejected : err= > { throw err }

    // Each call to THEN returns a brand new Promise, putting the logic previously collected for the THEN callbacks into new Promise()
    const promise2 = new Promise((resolve, reject) = > {
      // Successful callback
      if (this.status === STATUS.fulfilled) {
        try {
          The return value of resolve is passed to the result of the new promise
          const x = onFulfilled(this.value)
          resolve(x)
        } catch (err) {
          The return value of reject is passed to the result of the failure of the new promise
          reject(err)
        }
      }
      // Failed callback
      if (this.status === STATUS.rejected) {
        try {
          const x = onRejected(this.reason)
          resolve(x) // The normal value returned by onRejected will be used as the reslove value of the next THEN (promise specification)
        } catch (err) {
          reject(err)
        }
      }
      // If it is a wait state, put the res,rej processing callback function into the corresponding queue
      if (this.status === STATUS.pennding) {
        // Put into the successful callback queue
        this.onResolvedCallbacks.push(() = > {
          // Additional logic
          if (this.status === STATUS.fulfilled) {
            try {
              const x = onFulfilled(this.value)
              resolve(x)
            } catch (err) {
              reject(err)
            }
          }
        })
        // Put into the failed callback queue
        this.onRejectedCallbacks.push(() = > {
          try {
            const x = onRejected(this.reason)
            resolve(x)
          } catch (err) {
            reject(err)
          }
        })
      }
    })
    // Returns a new promise for the next THEN call
    returnpromise2; }}// case3
const promise = new Promise((reslove, reject) = > {
  setTimeout(() = > {
    reslove('success')},1000)
})
promise.then(
  value= > {
    console.log(value, '1')
    return 'returnValue'
  }
).then(
  value= > {
    console.log(value, '2')
    return 'otherValue'
  }
).then().then( // Empty then does not affect the value of return
  value= > console.log(value, '3'))// => one second later:
// success1
// returnValue2
// otherValue3

// case4
const promise1 = new Promise((reslove, reject) = > {
  setTimeout(() = > {
    reject('error')},1000)
})
promise1.then(
  value= >{},err= > {
    console.log(err, 'err')
    throw new Error('Wrong')
  }
).then(
  value= > {
    console.log(err, 's1')},err= > {
    console.log(err, 'e1')
  }
).then().then(
  value= > {
    console.log(err, 's2')},err= > {
    console.log(err, 'e2')})// => one second later:
// error
// Error
// The following is no longer executed
Copy the code

2. So far, the function of Promise has been basically realized. Multiple THEN chain calls can be made, and the value of return in THEN can be correctly transmitted to the next THEN, and throw Error in THEN can correctly interrupt the later THEN. However, there are some important features that cannot be mixed with standard promises. For example, if then returns a Promise instance, resolve/reject can be passed to the next THEN.


2.3 Promise Solution

The Promise resolver is an abstract operation that takes as input a promise and a value, which we express as [[Resolve]](promise, x). If x is a Thenable, it will try to get the Promise to adopt x’s state, provided x behaves at least somewhat like a promise. Otherwise, it will implement the promise with the value x. The handling of these Thenables allows interoperation with the promise implementation. As long as they expose then methods that match Promise/A+. It also allows promises/A+ implementations to “assimilate” inconsistent implementations with sound THEN methods. To run [[Resolve]](promise, x), do the following: 2.3.1 If a Promise and X are the same object, reject the promise with Tyeperror as reason. 2.3.2 If X is a Promise, use its state:

2.3.2.1 If X is in the pending state, the Promise must remain pending until X is in the fulfilled or Rejected state. 2.3.2.2 If X is fulfilled, the same value will fulfill promise. 2.3.2.3 If X is in the Rejected state, reject promise is rejected with the same reason.Copy the code

2.3.3 Otherwise, if x is an object or function:

2.3.3.1 Let THEN be x. teng. 2.3.3.2 If attribute X. teng causes exception E to be thrown, e is a Reason Reject promise. 2.3.3.3 If then is a function, let x be called as this with the first argument resolvePromise and the second argument rejectPromise, then: 2.3.3.3.1 Run [[Resolve]](promise, y) to invoke resolvePromise using value y. 2.3.3.3.2 If Reason R is used to invoke rejectPromise, r rejectPromise is also used. 2.3.3.3.3 If both resolvePromise and rejectPromise are invoked, or if the same parameter is invoked more than once, then the first call takes precedence and all other calls are ignored. 2.3.3.3.4 If an unexpected E is thrown during the call to THEN, 2.3.3.3.4.1 If resolvePromise or rejectPromise is called, ignore it. 2.3.3.3.4.2 Otherwise, use eas reason reject promise. 2.3.3.4 If THEN is not a function, implement the promise with x as an argument.Copy the code

2.3.4 If x is not an object or function, implement the promise with x as an argument. If a Thenable participating in the Thenable loop chain goes to resolve the promise, The recursive nature of [[Resolve]](promise, thenable) will eventually cause [[Resolve]](promise, thenable) to be called again, and following the algorithm will result in infinite recursion. We encourage (but do not require) detection of such recursions, and reject promises with TypeError as reason. [3.6]

  1. Pay attention to

3.1 “Platform code” refers to the engine, environment, and Promise implementation code. In fact, this requirement ensures that the onFulfilled and onRejected will be executed asynchronously, which is called with a new stack after the event loop. This can be done through “macro task” mechanisms (such as settimeou t or Setimmediate) or “microtask” mechanisms (such as MutationObserver or process.nextick). Because the Promise implementation is considered platform code, it may itself contain a task scheduling queue, or “trampoline,” in which handlers are invoked. 3.2 That is, in strict mode, this will be undefined inside them; In sloppy mode it will be a global object. 3.3 An implementation may allow promisE2 == promisE1 if it satisfies all requirements. Each implementation should document whether it can generate promisE2 == promisE1 and under what conditions. 3.4 In general, X is only known to be a true promise if it comes from the current implementation. This clause allows the use of implementation-specific methods to adopt a state of known consistent commitment. 3.5 This procedure first stores a reference to x, then tests the reference, and then invokes the reference to avoid multiple access to the x. teng property. These precautions are important to ensure consistency of accessor properties, whose values may change between retrievals. 3.6 The implementation should not set any subjective limit on the depth of the chain, and it should not assume that the depth of the chain will be infinite beyond the subjective limit. Only true loops can cause TypeError. If you encounter a chain of infinitely many different Thenable’s, recursion is always the correct behavior.

Consider: It is possible to return a new promise in then. In this case, you need to pass the reslove or Reject value of the promise to the next THEN, and the promise may be implemented by others, so it needs to be compatible. So we need the Promise resolution function and the logic at the heart of it is to resolve the type of X, to determine whether the promise2 will succeed or fail, to determine the value of x to determine the relationship between the promise2 and the possibility that x will be someone else’s Promise and that someone else’s Promise will go wrong. Let’s implement the reslovePromise function

// Resolve the type of X to determine whether it succeeds or fails
Promise2: promise instance in the current then
// x: the return value of the previous THEN
// resolve,reject: promise executor in the current then
function resolvePromise(promise2, x, resolve, reject) {
    // The value of x determines the relationship between the promise2 and the possibility that x is someone else's promise or someone else's promise is going to go wrong
    if (x == promise2) {
        return reject(new TypeError('Wrong'))}/ / {}
    if ((typeof x === 'object'&& x ! = =null) | |typeof x === 'function') {
        // X can only be a promise if x is an object or function
        let called = false; // Indicates that no call succeeds or fails
        try {
            let then = x.then; // take the then method on x
            if (typeof then == 'function') { // {then:function(){}}
                then.call(x, y= > { // x.teng can also use the get method
                    // y may be a promise, resolving the value of y recursively until it is a normal value
                    if (called) return;
                    called = true
                    resolvePromise(promise2, y, resolve, reject);
                }, r= > {
                    if (called) return;
                    called = true
                    reject(r);
                });
            }else{
                resolve(x); // Common object {}}}catch (e) {
            if (called) return;
            called = true
            reject(e); // Go to the failure logic}}else {
        // If not, it must be a normal valueresolve(x); }}Copy the code

So the old Promise class could look like this:

// Define the state enumeration
const STATUS = {
  pennding: 'PENNDING'.fulfilled: "FULFILLED".rejected: 'REJECTED'
}

class Promise{
  constructor(executor) { // Executor is new Promise((reslove,reject)=>{}) (reslove,reject)=>{... } function
    
    this.status = STATUS.pennding // The current state is created pennding
    this.value = null / / the current value
    this.reason = null // Failure cause
    
    this.onReslovedCallbacks = [] // Store the successful callback queue
    this.onRejectedCallbacks = [] // Store the failed callback function queue

    / / reslove function
    const reslove = (value) = > {
      if (this.status === STATUS.pennding) {
        this.status = STATUS.fulfilled // This will be a big pity
        this.value = value
        // Perform the depressing functions for all subscriptions
        this.onReslovedCallbacks.forEach(fn= > fn())
      }
    }
    
    / / reject function
    const reject = (reason) = > {
      if (this.status === STATUS.pennding) {
        this.status = STATUS.rejected // This will be a big pity
        this.reason = reason
        // Execute the Rejected function for all subscriptions
        this.onRejectedCallbacks.forEach(fn= > fn())
      }
    }
    
    // Catch exceptions to handle errors thrown by user reslove/reject, such as throw Error(' XXXXX ')
    try {
      executor(reslove, reject)
    } catch (err) {
      reject(err)
    }
  }
  
  // then (promise); // then (promise)
  then(onFulfilled, onRejected) {
    // Handle onFulfilled, onRejected is empty, and prevent THEN from losing the value returned by the last THEN
    onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : x= > x;
    onRejected = typeof onRejected == 'function' ? onRejected : err= > { throw err }
    
    // Each call to THEN returns a brand new Promise, putting the logic previously collected for the THEN callbacks into new Promise()
    const promise2 = new Promise((reslove, reject) = > {
      // Successful callback
     if (this.status == STATUS.fulfilled) {
       setTimeout(() = > { // Because resolvePromise uses promisE2, the resolvePromise is not instantiated yet, so it needs to be wrapped with timer
         try {
           let x = onFulfilled(this.value); // x could be a promise
           resolvePromise(promise2, x, resolve, reject);
           // resolve(x); // Use the return value of then as the successful result of the next THEN
         } catch(e) { reject(e); }},0);
     }
      if (this.status === STATUS.rejected) {
        setTimeout(() = > {
          try {
            let x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject);
          } catch(e) { reject(e); }},0);
      }
      if (this.status == STATUS.pending) {
        / / subscribe
        this.onResolvedCallbacks.push(() = > { / / section
          // todo .. Extra logic
          setTimeout(() = > {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch(e) { reject(e); }},0);
        });
        this.onRejectedCallbacks.push(() = > {
          setTimeout(() = > {
            try {
              let x = onRejected(this.reason)
              resolvePromise(promise2, x, resolve, reject);
            } catch(e) { reject(e); }},0); })}})// Returns a new promise for the next THEN call
    returnpromise2; }}// case5
const promise = new Promise((reslove,reject) = >{
  settimeout(() = >{
    reslove('success')},1000)
})
promise.then(
  value= > {
    consoleThe log (value,'1')
    return 'returnValue'
  }
).then(
  value= > console.log(value, '2')
  return 'otherValue'
).then().then( // Empty then does not affect the value of return
  value= > console.log(value, '3'))// => one second later:
// success1
// returnValue2
// otherValue3

// case6
let promise = new Promise((resolve, reject) = > {
  resolve('ok')
}).then(data= > {
  return new Promise((resolve, reject) = > {
    setTimeout(() = > {
      resolve(new Promise((resolve, reject) = > {
        setTimeout(() = > {
          resolve(data + ' resolved');
        }, 2000);
      }));
    }, 1000)})},err= >{}); promise.then(data= > {
    console.log('success', data);
  },
  err= > {
    console.log('fail', err); })// => 2 seconds later:
// success ok resolved
Copy the code

At this point, A simple Promise implementation that conforms to the A+ specification is complete. With that in mind, you can implement some of your own little Promise features, such as the Deferred function:

Promise.prototype.deferred = function () {
    let dfd = {} as any;
    dfd.promise = new Promise((resolve,reject) = >{
        dfd.resolve = resolve;
        dfd.reject = reject;
    })
    return dfd;
}

// case7
let dfd = Promise.deferred(); // Delay object
setTimeout(() = >{
  dfd.resolve('dfd')},1000)
dfd.promise.then(
  data= >{
    console.log(data)
  },
)
// Print DFD after one second
Copy the code

You can also implement the promise.all method

Promise.prototype.all = function(values) {
  return new Promise((resolve, reject) = > {
    let arr = [];
    let times = 0;
    function collectResult(val, key) {
      arr[key] = val;
      if(++times === values.length) { resolve(arr); }}for (let i = 0; i < values.length; i++) {
      let value = values[i];
      if (value && isPromise(value)) {
        value.then((y) = > {
          // Note: we want to use a counter here, not push directly
          collectResult(y, i)
        }, reject)
      } else{ collectResult(value, i); }}})}Copy the code

End scatter flower ~