preface

Back in the early days of Node writing, async callbacks were used, and while they were avoided as much as possible, callback hell can still occur when the functionality is complex.

Luckily, there was a promise. Recently, I had a chance to study the promise implementation. So I thought I’d try to do it manually, and make a note of it.

The function of the promise

Implementation of the constructor

Let’s start by writing a bit of promise code to analyze the constructor implementation:

let pro = new Promise((resolve, reject) = > {
  if (true) {
    resolve('the value')}else {
    reject('the reason')}})Copy the code
  • There are three states of Promise: pending, fulfilled and rejected. Once the state is determined, it cannot be changed. The initial state is pending, and there are only two types of state changes:
    • pending –> fulfilled
    • pending –> rejected
  • The promise constructor, passing as an argument a function that accepts the two functions returned by promise, resolve and reject, and changes the state when called.

You can write the following constructor:

const PENDING = 'pending' / /
const FULFILLED = 'fulfilled' / / has been successful
const REJECTED = 'rejected' / / has failed

class MyPromise {
  constructor(executor) {
    executor(this.resolve, this.reject)
  }
  // The initial state is PENDING
  status = PENDING

  resolve() {
    // Only PENDING states can be FULFILLED
    if (this.status ! == PENDING)return
    // The status changes to success
    this.status = FULFILLED
  }
  reject() {
    // Only PENDING state can change to REJECTED state
    if (this.status ! == PENDING)// The status changes to failed
    this.status = REJECTED
  }
}
Copy the code

Implementation of then methods

Call the promise written above

pro.then(res= > {
    console.log(res)
}, (reason) = > {
    console.log(reason)
})
Copy the code
  • The promise prototype has a THEN method. The then method accepts both a successful callback and a failed callback. The then method internally determines the state and calls the success callback if it is successful and the failure callback if it is failed.
  • The constructor, if called resolve, passes a value as an argument to a successful callback in the then method. Reject, if called, passes a value as an argument to the then method’s failed callback.

You can write the following code:

const PENDING = 'pending' / /
const FULFILLED = 'fulfilled' / / has been successful
const REJECTED = 'rejected' / / has failed

class MyPromise {
  constructor(executor) {
    executor(this.resolve, this.reject)
  }
  // The initial state is PENDING
  status = PENDING
  // Value after success
  value = undefined
  // The cause of the failure
  reason = undefined

  resolve = (value) = > {
    // Only PENDING states can be FULFILLED
    if (this.status ! == PENDING)return
    // The status changes to success
    this.status = FULFILLED

    this.value = value
  }
  reject = (reason) = > {
    // Only PENDING state can change to REJECTED state
    if (this.status ! == PENDING)// The status changes to failed
    this.status = REJECTED

    this.reason = reason
  }
  then = (successCallback, failCallback) = > {
    if (this.status === FULFILLED) {
      successCallback(this.value)
    } else {
      failCallback(this.reason)
    }
  }
}
Copy the code

Tips: Each step can be called by itself to verify that the relevant functionality has been implemented.

Improved THEN support for asynchrony

It is clear that our method does not support asynchrony, so we implement asynchrony:

let pro = new MyPromise((resolve, reject) = > {
  setTimeout(() = > {
    resolve(100)},1000)
})

pro.then((res) = > {
  console.log(res)
})
Copy the code
  • It is obvious that we need to call resolve during the above execution inside setTimeout to change the state. The then method will be called first, so we should add a pending state determination to the THEN method, and cache the incoming functions of the THEN method. Wait until the resolve call. The implementation is as follows:
 // Cache success callback function
  successCallback = null
  // Cache failure callback function
  failCallback = null

  resolve = (value) = > {
    // Only PENDING states can be FULFILLED
    if (this.status ! == PENDING)return
    // The status changes to success
    this.status = FULFILLED

    this.value = value
    // The callback was successfully executed
    if (this.successCallback) this.successCallback(this.value)
  }
  reject = (reason) = > {
    // Only PENDING state can change to REJECTED state
    if (this.status ! == PENDING)// The status changes to failed
    this.status = REJECTED

    this.reason = reason
    // Failed to execute the callback
    if (this.failCallback) this.failCallback(this.reason)
  }
  then = (successCallback, failCallback) = > {
    if (this.status === FULFILLED) {
      successCallback(this.value)
    } else if (this.status === REJECTED) {
      failCallback(this.reason)
    } else {
       /** * Pending successful callback and failed callback ** async case handling ** */
      this.successCallback = successCallback
      this.failCallback = failCallback
    }
  }
Copy the code

Chain calls

Promise supports chained calls, and the return value from the previous step is passed in as an argument to the next step:

pro.then((res) = > {
  console.log(res) / / 100
  return 1000
}).then(res= > {
  console.log(res) / / 1000
  return 10000
}).then(res= > {
  console.log(res) / / 10000
}) 
//
pro.then().then((res) = > console.log(res)) / / 100
Copy the code
  • To do this, then calls return a promise, because the then method on a promise is called promise2, and then is called then2.

    let promise2 = new MyPromise((resolve, reject) = > {})
    Copy the code
  • The timing of the return resolve/Reject call should be based on the successCallback/failCallback passed in by THEN2.

  • There are successful, failed, and asynchronous cases

    • Success and failure states:
      • Determine whether the value of result is a normal value or a promise object:
        • If the value is normal, call resolve directly
        • If it’s a Promise object, look at the result returned by the Promise object and decide whether to call resolve or reject
    • Asynchronous:
      • Temporary success callback and failed callback
      • If the resolve call is resolved, call the cached callbacks one by one
  • When a chained call passes no arguments, the value is passed downwards, so we start with a judgment inside then that if there is no success or failure callback, we return the value directly, or the cause of the failure:

    /** * then no arguments are passed ***/ 
     successCallback = successCallback ? successCallback:value= > value
     failCallback = failCallback ? failCallback:reason= > { throw reason }
    Copy the code

    The concrete implementation is as follows:

      // Cache success callback function
      successCallback = []
      // Cache failure callback function
      failCallback = []
    
      resolve = (value) = > {
        // Only PENDING states can be FULFILLED
        if (this.status ! == PENDING)return
        // The status changes to success
        this.status = FULFILLED
    
        this.value = value
        if (this.successCallback.length) this.successCallback.shift()(this.value)
      }
      reject = (reason) = > {
        // Only PENDING state can change to REJECTED state
        if (this.status ! == PENDING)// The status changes to failed
        this.status = REJECTED
    
        this.reason = reason
    
        if (this.failCallback.length) this.failCallback.shift()(this.reason)
      }
      then = (successCallback, failCallback) = > {
        /** * then no arguments are passed ** */ 
        successCallback = successCallback ? successCallback:value= > value
        failCallback = failCallback ? failCallback:reason= > { throw reason }
        const promise2 = new MyPromise((resolve, reject) = > {
          if (this.status === FULFILLED) {
            /** * determine whether result is a normal value or a promise object: If it's a normal value, call resolve directly. If it's a Promise object, look at the result returned by the Promise object and decide whether to call resolve or reject
            let result = successCallback(this.value)
            checkPromise(result, resolve, reject)
          } else if (this.status === REJECTED) {
            let result = failCallback(this.reason)
            checkPromise(result, resolve, reject)
          } else {
            /** * Pending successful callback and failed callback ** async case handling ** */
            this.successCallback.push(() = > {
              /** * determine whether result is a normal value or a promise object: If it's a normal value, call resolve directly. If it's a Promise object, look at the result returned by the Promise object and decide whether to call resolve or reject
              let result = successCallback(this.value)
              checkPromise(result, resolve, reject)
            })
            this.failCallback.push(() = > {
              /** * determine whether result is a normal value or a promise object: If it's a normal value, call resolve directly. If it's a Promise object, look at the result returned by the Promise object and decide whether to call resolve or reject
              let result = failCallback(this.value)
              checkPromise(result, resolve, reject)
            })
          }
        })
        return promise2
      }
      function checkPromise(result, resolve, reject) {
        if (result instanceof MyPromise) {
          // The result is a promise object
          result.then(resolve, reject)
        } else {
          // The result is a normal value
          resolve(result)
        }
      }
      
    Copy the code

    Now that we’ve implemented the core methods of Promise, let’s supplement some of the invocation methods of Promise.

    Catch implementation

    First, for code robustness, we want try-catch exception catching.

  • When constructing, you call the Reject method directly when you catch an exception.

    try {
       executor(this.resolve, this.reject)
    } catch (error) {
       this.reject(error)
    }
    Copy the code
  • In the THEN method, when a success or failure callback is called, the reject method is used directly if an exception is caught.

    try {
    	let result = successCallback(this.value)
    	checkPromise(result, resolve, reject)
    } catch(error) {
    	reject(error)
    }
    Copy the code

    Take a look:

      pro.then()
         .catch(e= >{})Copy the code
  • A catch is a prototype of a promise method that accepts a failure callback, implemented as follows:

     catch = (failCallback) = > {
        this.then(null, failCallback)
      }
    Copy the code