Full implementation of promise

Time:2021-7-30

Under developmentPromiseIs its common syntax. Basically, asynchronous processing is mostly throughPromiseTo complete. Promise has many specifications, and ES6 finally adoptsPromise / A + specificationTherefore, the following code is basically written based on this specification.

First, we list all instance methods and static methods of promise

Example method

  • then: New promise ((resolve, reject) = > {...}). Then (() = > {console. Log ('rsolve successful callback ')}, () = > {console. Log ('reject failed callback')})
  • catch: New promise ((resolve, reject) = > {...}). Catch (() = > {console. Log ('reject failed method ')})
  • finally: New promise ((resolve, reject) = > {...}). Finally (() = > {console. Log ('success or failure ')})
  • All of the above method calls will return a newPromise

Static method

  • resolve: Promise.resolve(value)returnPromiseexample
  • reject: Promise.reject(value)returnPromiseexample
  • all: Promise.all(promises): passed in array formatPromiseAnd return to the newPromiseInstance, the values will be returned in sequence after success, and one of the failures will directly become a failure
  • race: Promise.race(promises): passed in array formatPromiseAnd return to the newPromiseInstance, success or failure depends on how the first one is completed

PromiseOnce the status is determined to change, it cannot change again. There are three statuses:pendingfulfilledrejected
PromiseThe implementation in the browser is placed in the micro task queue, and the micro task needs to be processed(Event loop mechanism in JavaScript

1. Declare promise instance method

class Promise {
  _value
  _state = 'pending'
  _queue = []
  constructor(fn) {
    if (typeof fn !== 'function') {
      throw 'Promise resolver undefined is not a function'
    }
    /* 
      new Promise((resolve, reject) => {
        Resolve: successful
        Reject: failed
      })
    */
    fn(this._resolve.bind(this), this._reject.bind(this))
  }

  //Receive 1-2 parameters. The first is a successful callback and the second is a failed callback
  then(onFulfilled, onRejected) {
    //It may have been resolved, because promise can resolve in advance and then register after the then method
    if (this._state === 'fulfilled') {
      onFulfilled?.(this._value)
      return
    }
    //Reject the same principle
    if (this._state === 'rejected') {
      onRejected?.(this._value)
      return
    }
    //Promise has not been completed yet. Push to a queue. When it is completed, execute the corresponding function in the queue
    this._queue.push({
      onFulfilled,
      onRejected,
    })
  }

  //Receive failed callback
  catch(onRejected) {
    //It is equivalent to directly calling the callback of then incoming failure
    this.then(null, onRejected)
  }

  //Callback executed successfully and failed
  finally(onDone) {
    const fn = () => onDone()
    this.then(fn, fn)
  }
  //Successfully resolve
  _resolve(value) {
    //Once the state is determined, it will not change any more
    if (this._state !== 'pending') return
    this._state = 'fulfilled'

    //Save the value and directly take the value when calling again, because promise will not change once it is determined
    this._value = value

    //Execute the parameters in the form of push function in the previous. Then method, so as to execute the corresponding method.
    this._queue.forEach((callback) => {
      callback.onFulfilled?.(this._value)
    })
  }

  //Failed to reject
  _reject(error) {
    //Once the state is determined, it will not change any more
    if (this._state !== 'pending') return
    this._state = 'rejected'
    this._value = error
    this._queue.forEach((callback) => {
      callback.onRejected?.(this._value)
    })
  }
}

Call logic:

  1. adoptthenMethod passes in a parameter in the form of a function, that isonFulfilled => then((onFulfilled, onRejected) => {...})
  2. staythenMethodonFulfilledFunction put_queueIn this set. = >this._queue.push({ onFulfilled, onRejected })
  3. When the asynchronous callback is completed, executeresolveFunction, which is called at this time_queueCollected throughthenMethod. These functions are executed uniformly, so that the asynchronous callback is completed and the corresponding functions are executedthenFunction in method
//Result printing
const p = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})
p.then((res) => {
  console.log(res) // => success
})

// reject
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('fail')
  }, 1000)
})
p1.catch((res) => {
  console.log(res) // => fail
})

// finally
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve()
  }, 1000)
})
p2.finally(() => {
  console.log('done') // => done
})

Online code demonstration

2. Micro task processing and return promise

a. Perform micro task processing

In browserPromiseAfter completion, it will be pushed into the micro task, so we also need to deal with this. Use in browserMutationObserver, node can useprocess.nextTick

class Promise {
  ...
  //Push micro task
  _nextTick(fn) {
    if (typeof MutationObserver !== ' Undefined ') {// the browser implements the effect of micro tasks through mutationobserver
      //This can be shared separately to avoid unnecessary overhead. Otherwise, nodes need to be generated every time.
      const observer = new MutationObserver(fn)
      let count = 1
      const textNode = document.createTextNode(String(count))
      observer.observe(textNode, {
        characterData: true
      })
      textNode.data = String(++count)
    } else if (typeof process.nextTick !== ' Undefined ') {// the node side is implemented through process.nexttick
      process.nextTick(fn)
    } else {
      setTimeout(fn, 0)
    }
  }
  //Successfully resolve
  _resolve(value) {
    //Once the state is determined, it will not change any more
    if (this._state !== 'pending') return
    //Push micro task
    this._nextTick(() => {
      this._state = 'fulfilled'
      this._value = value
      this._queue.forEach((callback) => {
        callback.onFulfilled?.(this._value)
      })
    })
  }

  //Failed to reject
  _reject(error) {
    //Once the state is determined, it will not change any more
    if (this._state !== 'pending') return
    //Push micro task
    this._nextTick(() => {
      this._state = 'rejected'
      this._value = error
      this._queue.forEach((callback) => {
        callback.onRejected?.(this._value)
      })
    })
  }
  ...
}

Effect demonstration

b. Return promise for chain call

usuallyPromiseIt can process multiple asynchronous requests, and sometimes there are interdependencies between requests.

For example:

const getUser = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({
        userId: '123'
      })
    }, 500)
  })
}

const getDataByUser = (userId) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // ....
      resolve({a: 1})
    }, 500)
  })
}

//Use
getUser().then((user) => {
  return getDataByUser(user.userId)
}).then((res) => {
  console.log(res)// {a: 1}
})

getDataByUserrely ongetUserThe requested user information is needed herePromiseChain call, let’s change our code

class Promise {
  constructor(fn) {
    fn(this._resolve.bind(this), this._reject.bind(this))
  }
  ...
  //1. At this time, the then method needs to return a new promise, because a chain call is required, and the next then method accepts the value of the previous then method
  //2. The returned promise must be a new promise, otherwise the status and returned results will be shared.
  //3. Take the return value in the previous then method as the value of the next promise resolve
  then(onFulfilled, onRejected) {
    //Return to the new promise
    return new Promise((resolve, reject) => {
      //It may have been resolved, because promise can resolve in advance, and then register behind the then method. At this time, you can directly return the value to the function
      if (this._state === 'fulfilled' && onFulfilled) {
        this._nextTick(onFulfilled.bind(this, this._value))
        return
      }
      if (this._state === 'rejected' && onRejected) {
        this._nextTick(onRejected.bind(this, this._value))
        return
      }
      /* 
        Save the parameters of the then method of the current promise together with the resolve and reject of the new promise for association.
        In this way, onfulfilled in the previous promise and resolve in the new promise can be associated together, and then assignment and other operations can be performed. Reject the same principle
      */
      this._queue.push({
        onFulfilled,
        onRejected,
        resolve,
        reject
      })
    })
  }
  //Reject the same principle
  _resolve(value) {
    //Once the state is determined, it will not change any more
    if (this._state !== 'pending') return

    //The above example actually returns a promise instead of a directly returned value. Therefore, we need to do a special treatment here.
    //It is the value of resolve(). If it is a promise object, we need to parse the promise result and pass the value to resolve
    if (typeof value === 'object' && typeof value.then === 'function') {
      //We can put the current_ The resolve method is passed on, because once the parameters in the then method are resolved by the next promise, the corresponding parameters of the then method will be executed, and then the corresponding values will be passed in.
      //In this way, the value in promise can be obtained
      // this._resove => obj.onFulfilled?.(this._value)
      // this._reject => obj.onRejected?.(this._value)
      value.then(this._resolve.bind(this), this._reject.bind(this))
      return
    }

    //Push micro task
    this._nextTick(() => {
      this._state = 'fulfilled'
      this._value = value
      this._queue.forEach((obj) => {
        //Accept onfulfilled return value
        const val = obj.onFulfilled?.(this._value)
        //Reoslve this value, and onfulfilled is the first parameter in the current promise then method: promise. Then ((RES) = > {console. Log (RES)})
        //Obj.resolve is the resolve function of the new promise, which passes the return value in the then method to the next promise
        obj.resolve(val)
      })
    })
  }
  ...
}

Effect demonstration

Call logic:

  1. Micro task adoptionMutationObserverFollowprocess.nextTickTo implement
  2. PromiseChain call, here throughthenIn method(onFulfilled, onRejected)Parameter and newly returnedPromiseMedium(resolve, reject)Linked together.
  3. Once the lastPromiseSuccessful, callonFulfilledFunction, you can putonFulfilledThe value returned in the new promise is placed in the resolve of the new promise.
  4. If encounteredresolveThe value of isPromiseObject, recursively parse, and then return the value

Complete code

class Promise {
  _value
  _state = 'pending'
  _queue = []
  constructor(fn) {
    if (typeof fn !== 'function') {
      throw new Error('Promise resolver undefined is not a function')
    }
    /* 
      new Promise((resolve, reject) => {
        Resolve: successful
        Reject: failed
      })
    */
    fn(this._resolve.bind(this), this._reject.bind(this))
  }

  //Receive 1-2 parameters. The first is a successful callback and the second is a failed callback
  then(onFulfilled, onRejected) {
    //Return to the new promise
    return new Promise((resolve, reject) => {
      //It may have been resolved, because promise can resolve in advance, and then register behind the then method. At this time, you can directly return the value to the function
      if (this._state === 'fulfilled' && onFulfilled) {
        this._nextTick(onFulfilled.bind(this, this._value))
        return
      }
      if (this._state === 'rejected' && onRejected) {
        this._nextTick(onRejected.bind(this, this._value))
        return
      }
      //Save the parameters of the then method of the current promise together with the resolve and reject of the new promise for association
      this._queue.push({
        onFulfilled,
        onRejected,
        resolve,
        reject
      })
    })
  }

  //Receive failed callback
  catch(onRejected) {
    return this.then(null, onRejected)
  }

  //Callback executed successfully and failed
  finally(onDone) {
    return this.then((value) => {
      onDone()
      return value
    }, (value) => {
      // console.log(value)
      onDone()
      throw value
    })
  }

  //Push micro task
  _nextTick(fn) {
    if (typeof MutationObserver !== ' Undefined ') {// browser
      //This can be shared separately to avoid unnecessary overhead. Otherwise, nodes need to be generated every time.
      const observer = new MutationObserver(fn)
      let count = 1
      const textNode = document.createTextNode(String(count))
      observer.observe(textNode, {
        characterData: true
      })
      textNode.data = String(++count)
    } else if (typeof process.nextTick !== 'undefined') { // node
      process.nextTick(fn)
    } else {
      setTimeout(fn, 0)
    }
  }
  //Successfully resolve
  _resolve(value) {
    //Once the state is determined, it will not change any more
    if (this._state !== 'pending') return

    //The above example actually returns a promise instead of a directly returned value. Therefore, we need to do a special treatment here.
    //If the object of resolve () is promise, we need to parse the promise result and pass the value to resolve
    if (typeof value === 'object' && typeof value.then === 'function') {
      //We can put the current_ The resolve method is passed on, because once the parameters in the then method are resolved by the next promise, the corresponding parameters of the then method will be executed, and then the corresponding values will be passed in.
      //In this way, the value in promise can be obtained
      // this._resove => obj.onFulfilled?.(this._value)
      // this._reject => obj.onRejected?.(this._value)
      value.then(this._resolve.bind(this), this._reject.bind(this))
      return
    }

    //Push micro task
    this._nextTick(() => {
      this._state = 'fulfilled'
      this._value = value
      this._queue.forEach((obj) => {
        //Use try catch to catch the internal error of onfulfilled function
        try {
          //Accept the return value of onfulfilled. If it does not exist, put this_ Pass value down
          const val = obj.onFulfilled ? obj.onFulfilled(this._value) : this._value
          //Reoslve this value, and onfulfilled is the first parameter in the current promise then method: promise. Then ((RES) = > {console. Log (RES)})
          //Obj.resolve is the resolve function of the new promise, which passes the return value in the then method to the next promise
          obj.resolve(val)
        } catch (e) {
          obj.reject(e)
        }
      })
    })
  }

  //Failed to reject
  _reject(error) {
    if (this._state !== 'pending') return
    this._nextTick(() => {
      this._state = 'rejected'
      this._value = error
      this._queue.forEach((obj) => {
        try {
          const val = obj.onRejected ? obj.onRejected(this._value) : this._value
          //After the current reject is executed, a new promise will be returned. It should be able to resolve normally. Therefore, resolve should be used here. Reject should not be used to make the next promise execute the failed process
          obj.resolve(val)
        } catch (e) {
          obj.reject(e)
        }
      })
    })
  }
}

Declare a static method of promise

There are four static methods:Promise.resolvePromise.rejectPromise.allPromise.race, the new promise is returned uniformly.

class Promise {
  ...
  /**
   *Direct resolve
   */
  static resolve(value) {
    //Yes, promise returns directly
    if (value instanceof Promise) {
      return value
    } else if (typeof value === 'object' && typeof value.then === 'function') {
      //The passed in object contains the then method
      const then = value.then
      return new Promise((resolve) => {
        then.call(value, resolve)
      })
    } else {
      //The normal return value directly returns the new promise value in resolve
      return new Promise((resolve) => resolve(value))
    }
  }

  /**
   *If you don't perform a special test, you can directly return to project. Reject.
   */
  static reject(value) {
    return new Promise((resolve, reject) => reject(value))
  }

  /**
   *Pass in the 'promise' in array format and return a new 'promise' instance. After success, the values will be returned in order. If one of them fails, it will directly become a failure
   */
  static all(promises) {
    return new Promise((resolve, reject) => {
      let count = 0
      let arr = []
      //Push to the array according to the corresponding subscript
      promises.forEach((promise, index) => {
        //Convert to promise object
        Promise.resolve(promise).then((res) => {
          count++
          arr[index] = res
          if (count === promises.length) {
            resolve(arr)
          }
        }, err => reject(err))
      })
    })
  }
  
  /**
   *Pass in 'promise' in array format and return a new 'promise' instance. Success or failure depends on the completion method of the first one
   */
  static race(promises) {
    return new Promise((resolve, reject) => {
      promises.forEach((promise, index) => {
        //Convert to promise object
        Promise.resolve(promise).then((res) => {
          //Who executes direct resolve or reject first
          resolve(res)
        }, err => reject(err))
      })
    })
  }
  ...
}

Promise implementation complete code

class Promise {
  _value
  _state = 'pending'
  _queue = []
  constructor(fn) {
    if (typeof fn !== 'function') {
      throw new Error('Promise resolver undefined is not a function')
    }
    /* 
      new Promise((resolve, reject) => {
        Resolve: successful
        Reject: failed
      })
    */
    fn(this._resolve.bind(this), this._reject.bind(this))
  }

  /**
   *Receive 1-2 parameters. The first is a successful callback and the second is a failed callback
   *
   * @param {*} onFulfilled
   * @param {*} onRejected
   * @return {*} 
   * @memberof Promise
   */
  then(onFulfilled, onRejected) {
    //Return to the new promise
    return new Promise((resolve, reject) => {
      //It may have been resolved, because promise can resolve in advance, and then register behind the then method. At this time, you can directly return the value to the function
      if (this._state === 'fulfilled' && onFulfilled) {
        this._nextTick(onFulfilled.bind(this, this._value))
        return
      }
      if (this._state === 'rejected' && onRejected) {
        this._nextTick(onRejected.bind(this, this._value))
        return
      }
      //Save the parameters of the then method of the current promise together with the resolve and reject of the new promise for association
      this._queue.push({
        onFulfilled,
        onRejected,
        resolve,
        reject
      })
    })
  }

  /**
   *Receive failed callback
   *
   * @param {*} onRejected
   * @return {*} 
   * @memberof Promise
   */
  catch(onRejected) {
    return this.then(null, onRejected)
  }

  /**
   *Callback executed successfully and failed
   *
   * @param {*} onDone
   * @return {*} 
   * @memberof Promise
   */
  finally(onDone) {
    return this.then((value) => {
      onDone()
      return value
    }, (value) => {
      onDone()
      //Direct error reporting enables you to catch errors in the try catch
      throw value
    })
  }

  /**
   *Direct resolve
   *
   * @static
   * @param {*} value
   * @return {*} 
   * @memberof Promise
   */
  static resolve(value) {
    if (value instanceof Promise) {
      return value
    } else if (typeof value === 'object' && typeof value.then === 'function') {
      //The passed in object contains the then method
      const then = value.then
      return new Promise((resolve) => {
        then.call(value, resolve)
      })
    } else {
      return new Promise((resolve) => resolve(value))
    }
  }

  /**
   *Reject directly. Under the test, reject is not specially handled in promise.reject
   *
   * @static
   * @param {*} value
   * @return {*} 
   * @memberof Promise
   */
  static reject(value) {
    return new Promise((resolve, reject) => reject(value))
  }

  /**
   *Pass in the 'promise' in array format and return a new 'promise' instance. After success, the values will be returned in order. If one of them fails, it will directly become a failure
   *
   * @static
   * @param {*} promises
   * @memberof Promise
   */
  static all(promises) {
    return new Promise((resolve, reject) => {
      let count = 0
      let arr = []
      if (Array.isArray(promises)) {
        if (promises.length === 0) {
          return resolve(promises)
        }
        promises.forEach((promise, index) => {
          //Convert to promise object
          Promise.resolve(promise).then((res) => {
            count++
            arr[index] = res
            if (count === promises.length) {
              resolve(arr)
            }
          }, err => reject(err))
        })
        return
      } else {
        reject(`${promises} is not Array`)
      }
    })
  }
  
  /**
   *Pass in 'promise' in array format and return a new 'promise' instance. Success or failure depends on the completion method of the first one
   *
   * @static
   * @param {*} promises
   * @return {*} 
   * @memberof Promise
   */
  static race(promises) {
    return new Promise((resolve, reject) => {
      if (Array.isArray(promises)) {
        promises.forEach((promise, index) => {
          //Convert to promise object
          Promise.resolve(promise).then((res) => {
            resolve(res)
          }, err => reject(err))
        })
      } else {
        reject(`${promises} is not Array`)
      }
    })
  }

  //Push micro task
  _nextTick(fn) {
    if (typeof MutationObserver !== ' Undefined ') {// browser
      //This can be shared separately to avoid unnecessary overhead. Otherwise, nodes need to be generated every time.
      const observer = new MutationObserver(fn)
      let count = 1
      const textNode = document.createTextNode(String(count))
      observer.observe(textNode, {
        characterData: true
      })
      textNode.data = String(++count)
    } else if (typeof process.nextTick !== 'undefined') { // node
      process.nextTick(fn)
    } else {
      setTimeout(fn, 0)
    }
  }
  //Successfully resolve
  _resolve(value) {
    //Once the state is determined, it will not change any more
    if (this._state !== 'pending') return

    //The above example actually returns a promise instead of a directly returned value. Therefore, we need to do a special treatment here.
    //If the object of resolve () is promise, we need to parse the promise result and pass the value to resolve
    if (typeof value === 'object' && typeof value.then === 'function') {
      //We can put the current_ The resolve method is passed on, because once the parameters in the then method are resolved by the next promise, the corresponding parameters of the then method will be executed, and then the corresponding values will be passed in.
      //In this way, the value in promise can be obtained
      // this._resove => obj.onFulfilled?.(this._value)
      // this._reject => obj.onRejected?.(this._value)
      value.then(this._resolve.bind(this), this._reject.bind(this))
      return
    }

    //Through the print test, if the resolve is performed directly in the thread, the status and value seem to change directly. The main process is not executed, and it is modified when the micro task is executed.
    //Therefore, the state change and value modification are removed from the micro task, and can only be processed through the micro task when the callback is taken
    this._state = 'fulfilled'
    this._value = value

    //Push micro task
    this._nextTick(() => {
      this._queue.forEach((obj) => {
        //Use try catch to catch the internal error of onfulfilled function
        try {
          //Accept the return value of onfulfilled. If it does not exist, put this_ Pass value down
          const val = obj.onFulfilled ? obj.onFulfilled(this._value) : this._value
          //Reoslve this value, and onfulfilled is the first parameter in the current promise then method: promise. Then ((RES) = > {console. Log (RES)})
          //Obj.resolve is the resolve function of the new promise, which passes the return value in the then method to the next promise
          obj.resolve(val)
        } catch (e) {
          obj.reject(e)
        }
      })
    })
  }

  //Failed to reject
  _reject(error) {
    if (this._state !== 'pending') return
    this._state = 'rejected'
    this._value = error

    this._nextTick(() => {
      this._queue.forEach((obj) => {
        try {
          //Function internal error capture passed in by user
          if (obj.onRejected) {
            const val = obj.onRejected(this._value)
            //After the current reject is executed, a new promise will be returned. It should be able to resolve normally. Therefore, resolve should be used here. Reject should not be used to make the next promise execute the failed process
            obj.resolve(val)
          } else {
            //Recursive pass reject error
            obj.reject(this._value)
          }
        } catch (e) {
          obj.reject(e)
        }
      })
    })
  }
}

Complete demonstration effect

Blog original address

Complete code of the project:GitHub

The above is the implementation scheme of promise. Of course, this is completePromises / A + specificationThere are differences. This is only for learning.

QQ group official account
Front end miscellaneous group
Full implementation of promise
White gourd Bookstore
Full implementation of promise

I have created a new group to learn from each other. Whether you are Xiaobai who is ready to enter the pit or students who are halfway into the industry, I hope we can share and communicate together.
QQ group: 810018802, click to join