The purpose of writing this article is to analyze the Promise source code, the cause is also the recent autumn recruit was asked to let handwritten Promise, in addition to see the Promise source code on the Internet or more or less small problems, that is, did not fully follow the Promise/A+ specification.

The code will fully use ES6 syntax, mainly divided into the following modules:

  • Holistic analysis (paves the way for code writing)
  • Implementation of the first version (the constructor is roughly functional)
  • Support for asynchronous and chained calls (perfect for then methods)
  • Implementing catch methods
  • Realize the Promise. The resolve ()
  • Realize the Promise. Reject ()
  • Realize the Promise. All ()
  • Realize the Promise. Race ()

1. Overall analysis

A Promise is a container with three states: PENDING, FULFILLED and REJECTED. It holds the result of a certain event (usually an asynchronous operation) that will end in the future.

  • The container status is not affected by external conditions
  • Once the state changes it will never change again, and you can get this result at any time

Here’s how Promise is used:

new Promise((resolve, reject) => { // ... // Execute resolve, Reject}). Then (res => {// resolve corresponding to trigger function execution}, Then (// support chain call res => {}).catch(err => console.log(err)) promise.resolve (); Promise.reject(); Promise.all([promise1, promise2, ...] ).then(); Promise.race([promise1, promise2, ...] ).then();Copy the code

It is not difficult to say that:

  • The Promise constructor takes a function parameter, exector, which takes resolve and reject and executes it immediately, changing the state through resolve/reject
  • When the state changes, trigger on the prototype chainThen, the catchmethods
  • The Promise class has static methodsResolve, Reject, All, and race

You can write roughly structured code:

class Promise {
  constructor(exector) {
    const resolve = () = >{}const reject = () = > {

    }
    exector(resolve, reject);
  }
  then(){}catch() {}static resolve(){}static reject(){}static all(){}static race(){}}Copy the code

You can then add code based on this.

Second, the implementation of the first version

The resolve and reject functions are refined by introducing three states, and exector(resolve, reject) is executed within the constructor:

// Define three states
const PENDING = 'PENDING';      / /
const FULFILLED = 'FULFILLED';  / / has been successful
const REJECTED = 'REJECTED';    / / has failed

class Promise {
  constructor(exector) {
    // Initialization state
    this.status = PENDING;
    // Place success and failure results on this for easy access to then and catch
    this.value = undefined;
    this.reason = undefined;

    const resolve = value= > {
      // You can change a state only if it is in progress
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value; }}const reject = reason= > {
      // You can change a state only if it is in progress
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason; }}Execute exector immediately
    // Pass internal resolve and reject to the executor. The user can call resolve and rejectexector(resolve, reject); }}Copy the code

Exector (resolve, reject); Execution may fail, so use try, reject, and reject.

constructor(exector) {
  // Initialization state
  this.status = PENDING;
  // Place success and failure results on this for easy access to then and catch
  this.value = undefined;
  this.reason = undefined;

  const resolve = value= > {
    if (this.status === PENDING) {
      // You can change a state only if it is in progress
      this.status = FULFILLED;
      this.value = value; }}const reject = reason= > {
    if (this.status === PENDING) {
      // You can change a state only if it is in progress
      this.status = REJECTED;
      this.reason = reason; }}// Modify the code
  try {
    Execute executor immediately
    // Pass internal resolve and reject to the executor. The user can call resolve and reject
    exector(resolve, reject);
  } catch(e) {
    // The executor executes an error, throwing out the error content rejectreject(e); }}Copy the code

This will be a pity and REJECTED state. Then receives two functions, which correspond to the FULFILLED and REJECTED states:

new Promise().then(
  res => {},
  err => {},
)
Copy the code

Note: Then and catch are microtasks, where setTimeout is used to simulate:

then(onFulfilled, onRejected) {
  // Then is a microtask, which is simulated by setTimeout
  setTimeout(() = > {
    if (this.status === FULFILLED) {
      // This is a big pity
      onFulfilled(this.value);
    } else if (this.status === REJECTED) {
      // This parameter is executed only in the REJECTED state
      onRejected(this.reason); }})}Copy the code

OK, the first version is complete:

// Define three states
const PENDING = 'PENDING';      / /
const FULFILLED = 'FULFILLED';  / / has been successful
const REJECTED = 'REJECTED';    / / has failed

class Promise {
  constructor(exector) {
    // Initialization state
    this.status = PENDING;
    // Place success and failure results on this for easy access to then and catch
    this.value = undefined;
    this.reason = undefined;

    const resolve = value= > {
      if (this.status === PENDING) {
        // You can change a state only if it is in progress
        this.status = FULFILLED;
        this.value = value; }}const reject = reason= > {
      if (this.status === PENDING) {
        // You can change a state only if it is in progress
        this.status = REJECTED;
        this.reason = reason; }}try {
      Execute executor immediately
      // Pass internal resolve and reject to the executor. The user can call resolve and reject
      exector(resolve, reject);
    } catch(e) {
      // The executor executes an error, throwing out the error content rejectreject(e); }}then(onFulfilled, onRejected) {
    // Then is a microtask, which is simulated by setTimeout
    setTimeout(() = > {
      if (this.status === FULFILLED) {
        // This is a big pity
        onFulfilled(this.value);
      } else if (this.status === REJECTED) {
        // This parameter is executed only in the REJECTED state
        onRejected(this.reason); }}}})Copy the code

You can test it with data:

const promise = new Promise((resolve, reject) = > {
  Math.random() < 0.5 ? resolve(1) : reject(-1);
}).then(
  res= > console.log(res),
  err= > console.log(err),
)
Copy the code

Support asynchronous and chain call

At this point, the first version still needs to be improved in three directions:

  1. Problems with asynchronous code execution within promises.
  2. The chain call to Promise
  3. The value passed through

Support for asynchronous code

In development, the interface is often placed inside the promise. When the interface request response is successful, the data is resolved or rejected, and then and catch will be captured.

However, in the current code, resolve will be executed only after asynchronous code is executed in promise, and then will not wait for the completion of asynchronous code execution, so the state is PENDING and the callback function of THEN will not be triggered.

Added onledCallbacks, onRejectedCallbacks Maintenance success state, failure state task queue:

// Define three states
const PENDING = 'PENDING';      / /
const FULFILLED = 'FULFILLED';  / / has been successful
const REJECTED = 'REJECTED';    / / has failed

class Promise {
  constructor(exector) {
    // Initialization state
    this.status = PENDING;
    // Place success and failure results on this for easy access to then and catch
    this.value = undefined;
    this.reason = undefined;
    
    // Add code:
    // Success callback function queue
    this.onFulfilledCallbacks = [];
    // Failed state callback function queue
    this.onRejectedCallbacks = [];

    const resolve = value= > {
      // You can change a state only if it is in progress
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        // Add code:
        // The success state functions are executed in sequence
        this.onFulfilledCallbacks.forEach(fn= > fn(this.value)); }}const reject = reason= > {
      // You can change a state only if it is in progress
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        // Add code:
        // The failed state functions are executed in sequence
        this.onRejectedCallbacks.forEach(fn= > fn(this.reason))
      }
    }
    try {
      Execute executor immediately
      // Pass internal resolve and reject to the executor. The user can call resolve and reject
      exector(resolve, reject);
    } catch(e) {
      // The executor executes an error, throwing out the error content rejectreject(e); }}then(onFulfilled, onRejected) {
    // Then is a microtask, which is simulated by setTimeout
    setTimeout(() = > {
      // Add code:
      if (this.status === PENDING) {
        // State is PENDING
        // Indicate that the promise has asynchronous code execution inside and has not changed its state. Add it to the success/failure callback task queue
        this.onFulfilledCallbacks.push(onFulfilled);
        this.onRejectedCallbacks.push(onRejected);
      }else if (this.status === FULFILLED) {
        // This is a big pity
        onFulfilled(this.value);
      } else if (this.status === REJECTED) {
        // This parameter is executed only in the REJECTED state
        onRejected(this.reason); }}}})const promise = new Promise((resolve, reject) = > {
  setTimeout(() = > resolve(1), 1000);
}).then(
  res= > console.log(res)
)
/ / 1
Copy the code

Implement chain calls

A big advantage of promises is that they support chained calls, specifically implementations of then methods that actually return a Promise. A few points to note:

  1. Save a reference to the previous Promise instance, that is, savethis
  2. According to thethenThe return value of the callback function execution
  • If it is a PROMISE instance, the next promise instance returned will wait for the promise state to change
  • If it is not a Promise instance, execute it as isresolveorreject

Perfecting the then function:

then(onFulfilled, onRejected) {
  / / save this
  const self = this;
  return new Promise((resolve, reject) = > {
    if (self.status === PENDING) {
      self.onFulfilledCallbacks.push(() = > {
        // try to catch an error
        try {
          // Simulate microtasks
          setTimeout(() = > {
            const result = onFulfilled(self.value);
            // There are two kinds of cases:
            // 1. The return value of the callback is Promise
            // 2. If it is not a Promise, call the new Promise's resolve function
            result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }}); self.onRejectedCallbacks.push(() = > {
        // Do the following
        try {
          setTimeout(() = > {
            const result = onRejected(self.reason);
            // Reject: reject
            result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }})}else if (self.status === FULFILLED) {
      setTimeout(() = > {
        try {
          const result = onFulfilled(self.value);
          result instanceof Promise ? result.then(resolve, reject) : resolve(result);
        } catch(e) { reject(e); }}); }else if (self.status === REJECT){
      setTimeout(() = > {
        try {
          const result = onRejected(self.error);
          result instanceof Promise ? result.then(resolve, reject) : resolve(result);
        } catch(e) { reject(e); }})}})}Copy the code

The value passed through

Promise support values through:

let promsie = new Promise((resolve,reject) = >{
  resolve(1)
})
.then(2)
.then(3)
.then(value= > {
  console.log(value)
})
/ / 1
Copy the code

The then argument is expected to be a function, and passing in a non-function will result in value penetration. Value pass-through can be understood as saying that a THEN is invalid if it is passed to something other than a function.

The principle is that if the promise passed in then is not a function, then the promise returns the value of the previous promise. This is how value penetration occurs, so you only need to set the two parameters of then:

onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value;
onRejected = typeof onRejected === 'function'? onRejected:
    reason= > { throw new Error(reason instanceof Error ? reason.message:reason) }
Copy the code

The complete then function code:

then(onFulfilled, onRejected) {
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value;
  onRejected = typeof onRejected === 'function'? onRejected:
    reason= > { throw new Error(reason instanceof Error ? reason.message:reason) }
  / / save this
  const self = this;
  return new Promise((resolve, reject) = > {
    if (self.status === PENDING) {
      self.onFulfilledCallbacks.push(() = > {
        // try to catch an error
        try {
          // Simulate microtasks
          setTimeout(() = > {
            const result = onFulfilled(self.value);
            // There are two kinds of cases:
            // 1. The return value of the callback is Promise
            // 2. If it is not a Promise, call the new Promise's resolve function
            result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }}); self.onRejectedCallbacks.push(() = > {
        // Do the following
        try {
          setTimeout(() = > {
            const result = onRejected(self.reason);
            // Reject: reject
            result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }})}else if (self.status === FULFILLED) {
      try {
        setTimeout(() = > {
          const result = onFulfilled(self.value);
          result instanceof Promise ? result.then(resolve, reject) : resolve(result);
        });
      } catch(e) { reject(e); }}else if (self.status === REJECTED){
      try {
        setTimeout(() = > {
          const result = onRejected(self.reason);
          result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }}}); }Copy the code

Implement the catch() method

Promise.prototype. Catch = promise.prototype. Then (null, onRejected)

catch(onRejected) {
  return this.then(null, onRejected);
}
Copy the code

Five, the Promise. The resolve ()

Regardless of the thenable object as the parameter, there are two cases for the parameter:

  1. PromiseThe instance
  2. notPromiseThe instance
static resolve(value) {
  if (value instanceof Promise) {
    // If it is a Promise instance, return it directly
    return value;
  } else {
    // If it is not a Promise instance, return a new Promise object, which is FULFILLED
    return new Promise((resolve, reject) = >resolve(value)); }}Copy the code

Six, Promise. Reject ()

Promise.reject also returns an instance of Promise with the state REJECTED.

Unlike promise.resolve, the arguments to the promise.reject method are left as reject arguments

static reject(reason) {
  return new Promise((resolve, reject) = >{ reject(reason); })}Copy the code

Seven, Promise. All ()

Return a promise object. The returned promise state is successful only if all promises are successful.

  1. All promise states change toFULFILLED, the returned Promise state becomesFULFILLED.
  2. A promise state changes toREJECTED, the returned promise state becomesREJECTED.
  3. Not all of the array members are promises, you need to use themPromise.resolve()To deal with.
static all(promiseArr) {
  const len = promiseArr.length;
  const values = new Array(len);
  // Record the number of successfully implemented promises
  let count = 0;
  return new Promise((resolve, reject) = > {
    for (let i = 0; i < len; i++) {
      // promise.resolve () to ensure that each is a Promise instance
      Promise.resolve(promiseArr[i]).then(
        val= > {
          values[i] = val;
          count++;
          // The state of the return promise can be changed if all execution is completed
          if (count === len) resolve(values);
        },
        err= >reject(err), ); }})}Copy the code

Eight, Promise. Race ()

Promise.race() is a simpler implementation:

static race(promiseArr) {
  return new Promise((resolve, reject) = > {
    promiseArr.forEach(p= > {
      Promise.resolve(p).then(
        val= > resolve(val),
        err= > reject(err),
      )
    })
  })
}
Copy the code

Ix. Complete code

// Simulate the implementation of Promise
// Promise solves callback hell with three tools:
// 1. Callback function delay binding
// 2. Return value through
// 3. Error bubbling

// Define three states
const PENDING = 'PENDING';      / /
const FULFILLED = 'FULFILLED';  / / has been successful
const REJECTED = 'REJECTED';    / / has failed

class Promise {
  constructor(exector) {
    // Initialization state
    this.status = PENDING;
    // Place success and failure results on this for easy access to then and catch
    this.value = undefined;
    this.reason = undefined;
    // Success callback function queue
    this.onFulfilledCallbacks = [];
    // Failed state callback function queue
    this.onRejectedCallbacks = [];

    const resolve = value= > {
      // You can change a state only if it is in progress
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        // The success state functions are executed in sequence
        this.onFulfilledCallbacks.forEach(fn= > fn(this.value)); }}const reject = reason= > {
      // You can change a state only if it is in progress
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        // The failed state functions are executed in sequence
        this.onRejectedCallbacks.forEach(fn= > fn(this.reason))
      }
    }
    try {
      Execute executor immediately
      // Pass internal resolve and reject to the executor. The user can call resolve and reject
      exector(resolve, reject);
    } catch(e) {
      // The executor executes an error, throwing out the error content rejectreject(e); }}then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value;
    onRejected = typeof onRejected === 'function'? onRejected :
      reason= > { throw new Error(reason instanceof Error ? reason.message : reason) }
    / / save this
    const self = this;
    return new Promise((resolve, reject) = > {
      if (self.status === PENDING) {
        self.onFulfilledCallbacks.push(() = > {
          // try to catch an error
          try {
            // Simulate microtasks
            setTimeout(() = > {
              const result = onFulfilled(self.value);
              // There are two kinds of cases:
              // 1. The return value of the callback is Promise
              // 2. If it is not a Promise, call the new Promise's resolve function
              result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }}); self.onRejectedCallbacks.push(() = > {
          // Do the following
          try {
            setTimeout(() = > {
              const result = onRejected(self.reason);
              // Reject: reject
              result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }})}else if (self.status === FULFILLED) {
        try {
          setTimeout(() = > {
            const result = onFulfilled(self.value);
            result instanceof Promise ? result.then(resolve, reject) : resolve(result);
          });
        } catch(e) { reject(e); }}else if (self.status === REJECTED) {
        try {
          setTimeout(() = > {
            const result = onRejected(self.reason);
            result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }}}); }catch(onRejected) {
    return this.then(null, onRejected);
  }
  static resolve(value) {
    if (value instanceof Promise) {
      // If it is a Promise instance, return it directly
      return value;
    } else {
      // If it is not a Promise instance, return a new Promise object, which is FULFILLED
      return new Promise((resolve, reject) = >resolve(value)); }}static reject(reason) {
    return new Promise((resolve, reject) = >{ reject(reason); })}static all(promiseArr) {
    const len = promiseArr.length;
    const values = new Array(len);
    // Record the number of successfully implemented promises
    let count = 0;
    return new Promise((resolve, reject) = > {
      for (let i = 0; i < len; i++) {
        // promise.resolve () to ensure that each is a Promise instance
        Promise.resolve(promiseArr[i]).then(
          val= > {
            values[i] = val;
            count++;
            // The state of the return promise can be changed if all execution is completed
            if (count === len) resolve(values);
          },
          err= >reject(err), ); }})}static race(promiseArr) {
    return new Promise((resolve, reject) = > {
      promiseArr.forEach(p= > {
        Promise.resolve(p).then(
          val= > resolve(val),
          err= > reject(err),
        )
      })
    })
  }
}
Copy the code