Promise is a new asynchronous solution in ES6, and it is often seen in daily development, as the native FETCH API is implemented based on Promise. So what are the features of promises, and how do you implement A promise with A promise/A+ specification?

Promise features

Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises

  • The state machine
    • There are three states: Pending, depressing and Rejected
    • This state can only be changed by pending -> depressing and pending -> Rejected. After the state is changed, it cannot be changed again
    • There must be an immutable value for success and an immutable reason for failure
  • The constructor
    • Promise accepts a function as an argument that takes two parameters fulfill and reject
    • Fulfill sets the promise state from pending to fulfilled and returns the result of the operation
    • Reject sets the promise state from Pending to Rejected, and returns an error
  • Then method
    • OnFulfilled and onRejected, which indicates the promise success and failure, respectively
    • The return value is passed as an argument to the next then method’s argument
  • Asynchronous processing
  • Chain calls
  • Other apis
    • The catch, finally
    • Resolve, Reject, Race, all, etc

implementation

Next we implement A promise step by step with the PROMISE /A+ specification

Basic implementation

Start by defining a constant that represents the three states of a promise

const STATE = {
  PENDING: 'pending'.FULFILLED: 'fulfilled'.REJECTED: 'rejected'
}
Copy the code

Then, two parameters value and Reason are initialized in promise, which respectively represent the value when the state is FULFILL and reject. Then, two functions are defined, which update the state and corresponding field value internally, and execute on success and failure respectively. We then pass these two functions to the constructor’s function arguments as follows:

class MyPromise {
  constructor(fn) {
    / / initialization
    this.state = STATE.PENDING
    this.value = null
    this.reason = null

    / / success
    const fulfill = (value) = > {
      // State can be changed only if state is pending
      if (this.state === STATE.PENDING) {
        this.state = STATE.FULFILLED
        this.value = value
      }
    }

    / / fail
    const reject = (reason) = > {
      if (this.state === STATE.PENDING) {
        this.state = STATE.REJECTED
        this.reason = reason
      }
    }
    Reject is called when there is an error executing the function
    try {
      fn(fulfill, reject)
    } catch (e) {
      reject(e)
    }
  }
}
Copy the code

If the current state is FulfulLED, the callback succeeds; if the current state is Rejected, the callback fails.

class MyPromise {
  constructor(fn) {
    / /...
  }

  then(onFulfilled, onRejected) {
    if (this.state === STATE.FULFILLED) {
      onFulfilled(this.value)
    }
    if (this.state === STATE.REJECTED) {
      onRejected(this.reason)
    }
  }
}

Copy the code

At this point a simple MyPromise is implemented, but at this point it can only handle synchronous tasks, not asynchronous operations

Asynchronous processing

To handle asynchronous operations, you can take advantage of the nature of queues by caching callback functions until the result of an asynchronous operation is returned.

In terms of concrete implementation, add judgment in then method. If the state is pending, write the incoming function to the corresponding callback function queue; Two arrays are used to hold queues of successful and failed callback functions respectively when the Promise is initialized, and they are augmented in the FULFILL and Reject callbacks. As follows:

class MyPromise {
  constructor(fn) {
    / / initialization
    this.state = STATE.PENDING
    this.value = null
    this.reason = null
    // Save the array
    this.fulfilledCallbacks = []
    this.rejectedCallbacks = []
    / / success
    const fulfill = (value) = > {
      // State can be changed only if state is pending
      if (this.state === STATE.PENDING) {
        this.state = STATE.FULFILLED
        this.value = value
        this.fulfilledCallbacks.forEach(cb= > cb())
      }
    }

    / / fail
    const reject = (reason) = > {
      if (this.state === STATE.PENDING) {
        this.state = STATE.REJECTED
        this.reason = reason
        this.rejectedCallbacks.forEach(cb= > cb())
      }
    }
    Reject is called when there is an error executing the function
    try {
      fn(fulfill, reject)
    } catch (e) {
      reject(e)
    }
  }

  then(onFulfilled, onRejected) {
    if (this.state === STATE.FULFILLED) {
      onFulfilled(this.value)
    }
    if (this.state === STATE.REJECTED) {
      onRejected(this.reason)
    }
    // When THEN is pending, write the two states to the array
    if (this.state === STATE.PENDING) {
      this.fulfilledCallbacks.push((a)= > {
        onFulfilled(this.value)
      })
      this.rejectedCallbacks.push((a)= > {
        onRejected(this.reason)
      })
    }
  }
}
Copy the code

Chain calls

The next step is to modify MyPromise to support chained calls. If you have used jquery and other libraries, you should be familiar with chained calls. The principle is that the caller returns itself, in this case the then method returns a promise. There is also the passing of return values:

class MyPromise {
  constructor(fn) {
    / /...
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((fulfill, reject) = > {
      if (this.state === STATE.FULFILLED) {
        // Pass the return value to the next FULFILL
        fulfill(onFulfilled(this.value))
      }
      if (this.state === STATE.REJECTED) {
        // Pass the return value into the next reject
        reject(onRejected(this.reason))
      }
      // When THEN is pending, write the two states to the array
      if (this.state === STATE.PENDING) {
        this.fulfilledCallbacks.push((a)= > {
          fulfill(onFulfilled(this.value))
        })
        this.rejectedCallbacks.push((a)= > {
          reject(onRejected(this.reason))
        })
      }
    })
  }
}
Copy the code

At this point, MyPromise already supports asynchronous operations, chain calls, and passing return values, which is a simplified version of a promise. Generally, when you need to write a promise by hand during an interview, this is enough. A full implementation of the Promise /A+ specification is also unrealistic in such A short time frame as an interview.

The full code up to this point is available in promise3.js

Promise/A + specification

OnFulfilled /onRejected returns A value X, which needs to be processed as follows:

  • If x is equal to the promise returned by the then method, throw oneTypeErrorerror
  • If x is onePromiseKeep the value of the promise returned by the then method consistent with the value of x
  • If x is an object or function, thenx.thenAssigned tothenAnd call the
    • ifthenIs a function, x is the scopethisCall, passing two argumentsresolvePromiserejectPromiseIf theresolvePromiserejectPromiseAre called or are called multiple times, the first call is adopted and the remaining calls are ignored
    • If the callthenIf the method fails, the thrown error E is used as the rejection reason to reject the promise
    • ifthenIf it is not a function, then the promise is executed with an x argument
  • If x is any other value, the promise is executed with x as an argument

Next, the MyPromise implemented in the previous step is further optimized to conform to the Promise /A+ specification:

class MyPromise {
  constructor(fn) {
    / /...
  }

  then(onFulfilled, onRejected) {
    const promise2 = new MyPromise((fulfill, reject) = > {
      if (this.state === STATE.FULFILLED) {
        try {
          const x = onFulfilled(this.value)
          generatePromise(promise2, x, fulfill, reject)
        } catch (e) {
          reject(e)
        }
      }
      if (this.state === STATE.REJECTED) {
        try {
          const x = onRejected(this.reason)
          generatePromise(promise2, x, fulfill, reject)
        } catch (e) {
          reject(e)
        }
      }
      // When THEN is pending, write the two states to the array
      if (this.state === STATE.PENDING) {
        this.fulfilledCallbacks.push((a)= > {
          try {
            const x = onFulfilled(this.value)
            generatePromise(promise2, x, fulfill, reject)
          } catch(e) {
            reject(e)
          }
        })
        this.rejectedCallbacks.push((a)= > {
          try {
            const x = onRejected(this.reason)
            generatePromise(promise2, x, fulfill, reject)
          } catch (e) {
            reject(e)
          }
        })
      }
    })
    return promise2
  }
}
Copy the code

Here we encapsulate the behavior of handling the return value x as a function generatePromise, which looks like this:

const generatePromise = (promise2, x, fulfill, reject) = > {
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'))}// If x is a promise, call its then method to continue the loop
  if (x instanceof MyPromise) {
    x.then((value) = > {
      generatePromise(promise2, value, fulfill, reject)
    }, (e) => {
      reject(e)
    })
  } else if(x ! =null && (typeof x === 'object' || typeof x === 'function')) {
    // To prevent repeated calls, success and failure can only be called once
    let called;
    // If x is an object or function
    try {
      const then = x.then
      if (typeof then === 'function') {
        then.call(x, (y) => {
          if (called) return;
          called = true;
          // y is a promise
          generatePromise(promise2, y, fulfill, reject)
        }, (r) => {
          if (called) return;
          called = true;
          reject(r)
        })
      } else {
        fulfill(x)
      }
    } catch(e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    fulfill(x)
  }
}
Copy the code

Promise2 = promise1. Then (onFulfilled, onRejected)

  • OnFulfilled /onRejected must be called asynchronously and cannot be synchronized
  • If ondepressing is not a function and promise1 executes successfully, promise2 must execute successfully and return the same value
  • If onRejected is not a function and promise1 rejects execution, promise2 must reject execution and return the same rejection

For the final improvement of then method, setTimeout is added to simulate asynchronous call, and the judgment of onFulfilled and onRejected methods is added:

class MyPromise {
  constructor(fn) {
    / /...
  }

  then(onFulfilled, onRejected) {
    // This is a pity and onRejected
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value
    onRejected = typeof onRejected === 'function' ? onRejected : e= > { throw e }
    const promise2 = new MyPromise((fulfill, reject) = > {
      // setTimeout Macro task, ensure that onFulfilled and onRejected are executed asynchronously
      if (this.state === STATE.FULFILLED) {
        setTimeout((a)= > {
          try {
            const x = onFulfilled(this.value)
            generatePromise(promise2, x, fulfill, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)}if (this.state === STATE.REJECTED) {
        setTimeout((a)= > {
          try {
            const x = onRejected(this.reason)
            generatePromise(promise2, x, fulfill, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)}// When THEN is pending, write the two states to the array
      if (this.state === STATE.PENDING) {
        this.fulfilledCallbacks.push((a)= > {
          setTimeout((a)= > {
            try {
              const x = onFulfilled(this.value)
              generatePromise(promise2, x, fulfill, reject)
            } catch(e) {
              reject(e)
            }
          }, 0)})this.rejectedCallbacks.push((a)= > {
          setTimeout((a)= > {
            try {
              const x = onRejected(this.reason)
              generatePromise(promise2, x, fulfill, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)})}})return promise2
  }
}
Copy the code

The complete promise code that implements the Promise /A+ specification is available as promise4.js

How do you know if the promise you implement follows the Promise /A+ specification? Promises – Aplus-Tests is an NPM package that can be used to do this

Other apis

Other commonly used Promise apis are implemented here

The catch, finally

class MyPromise {
  constructor(fn) {
    / /...
  }
  then(onFulfilled, onRejected) {
    / /...
  }
  catch(onRejected) {
    return this.then(null, onRejected)
  }
  finally(callback) {
    return this.then(callback, callback)
  }
}
Copy the code

Promise.resolve

Return a Promise object in the Resolved state

MyPromise.resolve = (value) = > {
  // Pass in the promise type and return it directly
  if (value instanceof MyPromise) return value
  // When the thenable object is passed in, the then method is immediately executed
  if(value ! = =null && typeof value === 'object') {
    const then = value.then
    if (then && typeof then === 'function') return new MyPromise(value.then)
  }
  return new MyPromise((resolve) = > {
    resolve(value)
  })
}
Copy the code

Promise.reject

Return a Promise object in the Rejected state

MyPromise.reject = (reason) = > {
  // Pass in the promise type and return it directly
  if (reason instanceof MyPromise) return reason
  return new MyPromise((resolve, reject) = > {
    reject(reason)
  })
}
Copy the code

Promise.race

Returns a promise that changes as soon as a promise state in the iterator changes

MyPromise.race = (promises) = > {
  return new MyPromise((resolve, reject) = > {
    // Promises are not arrays, but must have an Iterator interface, so use for... Of traversal
    for(let promise of promises) {
      // If the current value is not a Promise, use the resolve method to make a Promise
      if (promise instanceof MyPromise) {
        promise.then(resolve, reject)
      } else {
        MyPromise.resolve(promise).then(resolve, reject)
      }
    }
  })
}
Copy the code

Promise.all

This is a big pity. The returned promise will become a big pity only when all the promises in the iterator become fulfilled. There will be a promise in the iterator rejected and the returned promise will become fulfilled

MyPromise.all = (promises) = > {
  return new MyPromise((resolve, reject) = > {
    const arr = []
    // The number returned
    let count = 0
    // Current index
    let index = 0
    // Promises are not arrays, but must have an Iterator interface, so use for... Of traversal
    for(let promise of promises) {
      // If the current value is not a Promise, use the resolve method to make a Promise
      if(! (promiseinstanceof MyPromise)) {
        promise = MyPromise.resolve(promise)
      }
      // Use closures to ensure that arrays are returned asynchronously
      ((i) = > {
        promise.then((value) = > {
          arr[i] = value
          count += 1
          if (count === promises.length || count === promises.size) {
            resolve(arr)
          }
        }, reject)
      })(index)
      / / the index increases
      index += 1}})}Copy the code

Promise.allSettled

Only after all the promises in the iterator return, will a fulfilled promise be returned, and the returned promise state will always be fulfilled, and will not return the Rejected state

MyPromise.allSettled = (promises) = > {
  return new MyPromise((resolve, reject) = > {
    const arr = []
    // The number returned
    let count = 0
    // Current index
    let index = 0
    // Promises are not arrays, but must have an Iterator interface, so use for... Of traversal
    for(let promise of promises) {
      // If the current value is not a Promise, use the resolve method to make a Promise
      if(! (promiseinstanceof MyPromise)) {
        promise = MyPromise.resolve(promise)
      }
      // Use closures to ensure that arrays are returned asynchronously
      ((i) = > {
        promise.then((value) = > {
          arr[i] = value
          count += 1
          if (count === promises.length || count === promises.size) {
            resolve(arr)
          }
        }, (err) => {
          arr[i] = err
          count += 1
          if (count === promises.length || count === promises.size) {
            resolve(arr)
          }
        })
      })(index)
      / / the index increases
      index += 1}})}Copy the code

If there are any mistakes in this article, you are welcome to criticize

reference

  • MDN-Promise
  • ECMAScript 6 Getting Started -Promise objects
  • 【 例 句 】Promises/A+ Promises
  • Complete example and test code: github/ LVQQ/Promise