Interview Promise

The context in which Promise appears

  1. Asynchronous callbacks are discontinuous, with multiple callbacks registered at the same time and execution time not linear enough
  const callback1 = function() {}
  const callback2 = function() {}
  const callback3 = function() {}
  
  const res1 = ajax(callback1)
  const res2 = ajax(callback2)
  const res3 = ajax(callback3)
Copy the code

All three callbacks are registered at the same time, but the timing of their calls is really variable

  1. The callback hell
  function triggerCb(cb1) {
    return function(cb2) {
         cb1()
         cb2()
    }
  }
Copy the code

Callback hell causes the following problems:

1. It is easy to cause extremely poor readability of the code, and the internal operation will be very chaotic

2. Each callback function has success and failure states, requiring each callback function to maintain a set of error mechanisms

Those familiar with design patterns should be aware of the chain of responsibility pattern. Promises are a typical example of asynchronous invocation in a chain of responsibility

I Promise.

The state of the Promise

Promise states include: pending, depressing, and Rejected

According to the official document:

A promise must be in one of three states: pending, fulfilled, or rejected.When pending, a promise:may transition to either the fulfilled or rejected state.When fulfilled, a promise:must not transition to any other state.must have a value, which must not change.When rejected, a promise:must not transition to any other state.must have a reason, which must not change. Here, “must not change” means immutable identity (i.e. ===), but does not imply deep immutability.

When the state changes from pending to fulfiiled or rejected, it is locked and cannot be backtracked or changed

Promise of thenable

The usage of Promise. Then must be familiar to everyone, but specific it contains what hidden points, today can be combed

1. The arguments to then are not necessarily callback functions and pass through values

The two arguments to then are themselves defined as resolvedFn and rejectedFn, but it can pass non-functional data

  var p = Promise.resolve('martin')
  
  p.then(1).then((res) = > {})
  p.then(new Error('error')).then((res) = > {})
Copy the code

As shown in the above example, the final result is 1, which indicates that when the THEN processes non-functional data, the PromiseResult of the original is directly passed to the next THEN (including rejectedFn).

2. Then returns a new Promise. Then returns a variable that is not the same as the Promise that called the THEN method

  var p = Promise.reject('martin')
  varp1 = p.then() p1 ! == p p1.PromiseStatus ==='fulfilled'
  p.PromiseStatus === 'rejected'
Copy the code

In the above example, p1 is the variable returned after P calls then. == p, the result is true, proving that then does not return a promise

then must return a promise;

The Promise. Then callback must be a Promise

3. Then can be called multiple times by a Promise

As mentioned above, then returns a new Promise. When we write code, we usually make chained calls that inherit from the previous Promise to generate a new Promise, but we can also use variable caching of promises and register several THEN callbacks on that variable

  var p = Promise.resolve('martin')
  
  p.then(() = > {})
  p.then(() = > {})
Copy the code

These two THEN methods are registered on P, so the state of P will act on both then methods

4. Then Returns the state

We know that the then method returns a new Promise, but does its state change? In most cases, the Promise returned by THEN is a pity state. However, as long as the THEN callback function throws an error, the new Promise state will be set to Rejected

  var p = Promise.reject('martin')
  
  var p1 = p.then(() = > {}) // This is very depressing
  var p2 = p.then(() = > { throw new Error('martin')})// Then it becomes Rejected
  var p3 = p.then(() = > { return new Error('martin')})// This is a pity. // Only throw can become rejected
Copy the code

Promise.catch

1. I catch the Promise in the rejected state

We said that multiple “then” can work on a promise, so can “catch”

  var p = Promise.reject('martin')
  
  p.then(() = > {}, function reject(){})
  p.catch(e= > e)
  p.catch(e= > e)
Copy the code

The second callback, reject, and the following two catches are executed. Reject is registered, and a catch is not executed. A new Promise is returned by then. When an error occurs, if the upstream Promise does not intercept, the downstream Promise will take over

  var p = Promise.reject('martin')
  
  var p2 = p.then(() = >{}).catch(function error(){})
  var p3 = p.then(() = >{}, () = >{}).catch(function error(){})
Copy the code

P2’s catch callback executes, but P3’s catch callback does not, because P3’s catch is blocked by the then-registered reject function

2. Promise.catch hijacks the then callback in case of an error

Then is hijacked by a catch

3. A catch returns a Promise state. A catch returns a Promise state as then

Promise.finally

1. Promise.finally state follows the previous Promise

The promise.finally state follows the previous Promise

  var p = Promise.reject('martin')
  
  var p1 = p.finally(() = > {})
Copy the code

The state of P1 will follow the state of P, so it is rejected

Write a Promise

  1. We’ll start by defining a class for a Promise that has two private values, state and value
  class AWeSomePromise {
    constructor() {
      this.PromiseState = 'pending'
      this.PromiseResult = void 0}}Copy the code
  1. Promise is instantiated with a callback that takes resolve and Reject, which assign a value to the passed value and change the state
  class AWeSomePromise {
    constructor(callback) {
      this.PromiseState = 'pending'
      this.PromiseResult = void 0
      
      if (typeof callback === 'function') {
          callback(this.resolve.bind(this), this.reject.bind(this))}}resolve(resvalue) {
       this.excuteStatus(resvalue, 'fulfilled')}reject(resvalue) {
       this.excuteStatus(resvalue, 'rejected')}excuteStatus(promiseResult, promiseState) {
      this.PromiseResult = promiseResult / / assignment
      this.PromiseState = promiseState // Change the state
      
      triggerFn() // Trigger thenable}}Copy the code
  1. Define a THEN method on the prototype, where you need to combine several of the features mentioned above in promise.THEN

    • Thenable returns a new Promise
      then(resolveFn, rejectFn) {
        this.newPromise = new AWeSomePromise() // then returns a new Promise
        
        return this.newPromise
      }
    Copy the code
    • The second and later thenable calls resolveFn or rejectFn directly based on the previous state
        then() {
          if(status ! = ='pending') {
            this.excuteThen()
          }
        }
        
        excuteThen() {
          setTimeout(() = > {
            if (this.PromiseState === 'fulfilled') this.triggerFn('fulfilled')
            else if (this.PromiseState === 'rejected') this.triggerFn('rejected')},10)}Copy the code
    • The parameters thenable receives can be non-functional data, and several Thenable can work simultaneously on a Promise instance
      get resolveFns() { return[]}/ / save resolveFn
      get rejectFns() { return[]}/ / in the same way
      then(resolveFn, rejectFn) {
          const isResolveFn = typeof resolveFn === 'function' // determine whether reslveFn is a function
          const isRejectFn = typeof rejectFn === 'function'
          function excuteFn(thenStatus) {
            return function(fn, newPromise) {
              const type = thenStatus === 'fulfilled' ? isResolveFunction : isRejectFunction
              try {
                newPromise.PromiseState = status = 'fulfilled'
                if (this.PromiseState === thenStatus && type) {
                  if (fn) { // Check whether the passed resolveFn is a function
                    value = fn(this.PromiseResult) // value is a global value, which we'll talk about in a second
                    if (typeof value === 'undefined') value = this.PromiseResult // If thenable does not return a value, the value will be passed through
                    newPromise.PromiseResult = value // Update the value of the next Promise (when a Promise is asynchronous)
                  } else {
                    value = this.PromiseResult
                  }
                }
                if (thenStatus === 'rejected' && !type) {
                  const errorCallbacks = newPromise.errorCallbacks
                  this.triggerErrorCallback(errorCallbacks) // Trigger the catch callback}}catch(err) {
                status = thenStatus
                value = err
                newPromise.PromiseState = status = 'fulfilled'
                setTimeout(() = > {
                  this.triggerErrorCallback(newPromise.errorCallbacks)
                }, 0)}this.triggerFn(null, newPromise.finallyCallbacks) // Finally callback is triggered}}this.resolveFns.push(excuteFn('fulfilled').bind(this, resolveFn, this.newPromise))
        this.rejectFns.push(excuteFn('rejected').bind(this, rejectFn, this.newPromise))
      }
      
      triggerFn(thenStatus, fn) { // Loop trigger resolveFns and rejectFns
        if(! fn) { fn = thenStatus ==='fulfilled' ? this.resolveFns : this.rejectFns
        }
        let currentFn = null
        while(currentFn = fn.shift()) { // We need to fetch the first callback from resolveFns and kick it out of the queue
          if (currentFn) {
            currentFn()
          }
        }
      }
      
      The resolve, reject, and excuteStatus operations need to be changed
      resolve(resvalue) {
        this.excuteStatus(resvalue, 'fulfilled'.this.triggerFn.bind(this.'fulfilled'))}reject(rejectvalue) {
        this.excuteStatus(rejectvalue, 'rejected'.() = > {
          this.triggerFn.bind(this.'rejected')})}excuteStatus(promiseResult, promiseStatus, cb){...setTimeout(() = > { // Since promise. thenable is a microtask, setTimeout is used to simulate it
          if (cb) {
            cb()
          }
          
          this.triggerFn(this.finallyCallbacks) / / triggers finallyCallbacks
        }, 0)}Copy the code
    • Thenable Transparent transmission of status and value

    We know that the status of the second and subsequent thenable registrations is not changed by the resolve and Reject functions, so we need to define several values in the global context to cache the status of the previous one

        let status = 'pending'
        let value = void 0 
        
        class AWeSomePromise {
           constructor(callback) {
             this.PromiseState = status // Assign the previous state directly
             this.PromiseResult = value // Change the last value directly
    
             if (typeof callback === 'function') {
               callback(this.resolve.bind(this), this.reject.bind(this))}}then() {
              function excuteFn(thenStatus) {...return function(fn, newPromise) {
                  try {
                    newPromise.PromiseState = status = 'fulfilled'
                    if (this.PromiseState === thenStatus && type) {
                      if (fn) { // Check whether the passed resolveFn is a function
                        value = fn(this.PromiseResult) // Value is a global value, and the value returned by the previous thenable is cached
                        if (typeof value === 'undefined') value = this.PromiseResult // If thenable does not return a value, the value will be passed through
                        newPromise.PromiseResult = value // Update the value of the next Promise (when a Promise is asynchronous)
                      } else {
                        value = this.PromiseResult
                      }
                    }
                  } catch(err) { status = thenStatus value = err } } ... }}}Copy the code

    So this might be a little bit convoluted, but let’s make sure we understand the relationship between new promises and promises

      var p = new AWeSomePromise()
      
      var p1 = p.then()
      p.newPromise === p1
    Copy the code

    The relationship is as follows: promise.newPromise === nextPromise So that the basic principles of Thenable have been developed, let’s look at the catch function

  2. The catch function is similar to the Thenable development process, It can also be developed based on features such as multiple catches acting on a single Promise instance, a catch hijacking thenable error, and a Reject that triggers a Promise in the Rejected state when the reject function is unregistered

      reject() {
        this.excuteStatus(rejectvalue, 'rejected'.() = > {
          this.triggerErrorCallback() // All catch callbacks registered under the current Promise need to be triggered
          this.triggerFn.bind(this.'rejected')})}get errorCallbacks() { return[]}catch(callback) {
        this.errorCallbacks.push(callback)
        
        return this.newPromise || (this.newPromise = new AWeSomePromise()) // Catch may not have registered thenable before, so this. NewPromise needs to be handled
      }
      
      triggerErrorCallback(errorCallbacks = this.errorCallbacks) {
        let currentFn = null
        while(currentFn = errorCallbacks.shift()) {
          if (currentFn) {
            currentFn(this.PromiseResult)
          }
        }
      }
      
      then() {
         function excute() {
           return function(fn, newPromise) {
             const type = thenStatus === 'fulfilled' ? isResolveFunction : isRejectFunction
             try{
              if (thenStatus === 'rejected' && !type) { Reject (reject); reject (reject); reject (reject)
                const errorCallbacks = newPromise.errorCallbacks
                this.triggerErrorCallback(errorCallbacks)
              }
             } catch(err) {
               newPromise.PromiseState = status = 'fulfilled'
               setTimeout(() = > {
                this.triggerErrorCallback(newPromise.errorCallbacks)
               }, 0)}}}}Copy the code
  3. The finally function executes whether resolve or reject is fired

    excuteStatus(cb) {
      setTimeout(() = > {
        if (cb) {
          cb()
        }
         
        this.triggerFn(this.finallyCallbacks) // The finally callback under the current Promise is invoked. Promise.finally ()
      }, 0)}then() {
      function excuteFn() {
        return function() {...this.triggerFn(null, newPromise.finallyCallbacks) // Then ().finally}}}Copy the code
  1. Dealing with asynchrony, which is the core case for Promise, lets start with an example of asynchrony:
  var p = new Promise(resolve= > {
    setTimeout(() = > { resolve('martin')},1000)
  })
  
  p.then(() = > {}).then(() = > {})...
Copy the code

The above p.tenable function and all subsequent chained thenable functions will be executed after 1000ms delay. According to the second and subsequent Thenable functions we wrote above, excuteThen will be triggered when the call is registered. ResolveFns will be triggered after a delay of 10ms, but the above case obviously needs to be delayed for another 1000ms. Therefore, we do not know that the resolve function will be delayed for a few seconds, but when it is executed, it means that the subsequent thenable callback can be executed. Therefore, we only need to cache the Promise instances returned by Thenable into the array, detect the subscript value of the current Promise instance in the array after the execution of resolve function, and trigger resolveFns one by one

  let promiseArray = []
  
  class AWeSomePromise {
    excuteStatus() {
      this.reconnect()
    }
     
    then() {
      promiseArray.push(newPromise)
    }
    
    reconnect() {
       if (promiseArray.length) {
         const index = promiseArray.indexOf(this)
         if (~index) {
           promiseArray.slice(index).forEach(context= > {
             if (context instanceof AWeSomePromise) {
               context.excuteThen()
             }
           })
         }
       }
     }
  }
Copy the code

In this way, we can preliminarily complete the simulation of Promise

The complete code

    let status = 'pending'
    let value = void 0
    let promiseArray = []
    class MyPromise {
      constructor(callback) {
        this.PromiseState = status
        this.PromiseResult = value
        this.resolveFns = []
        this.rejectFns = []
        this.errorCallbacks = []
        this.finallyCallbacks = []
        this.done = false
        if (callback) {
          callback(this.resolve.bind(this), this.reject.bind(this))}}resolve(resvalue) {
        this.excuteStatus(resvalue, 'fulfilled'.this.triggerFn.bind(this.'fulfilled'))}reject(rejectvalue) {
        this.excuteStatus(rejectvalue, 'rejected'.() = > {
          this.triggerErrorCallback()
          this.triggerFn.bind(this.'rejected')})}excuteStatus(promiseResult, promiseStatus, cb) {
        this.PromiseResult = value = promiseResult
        status = promiseStatus
        this.PromiseState = status
        if (this.newPromise) {
          this.newPromise.PromiseResult = promiseResult
          this.newPromise.PromiseState = promiseStatus
        }
        this.reconnect()

        setTimeout(() = > {
          if (cb) {
            cb()
          }
          
          this.triggerFn(this.finallyCallbacks)
        }, 0)}then(resolveFn, rejectFn) {
        this.newPromise = new MyPromise()
        const isResolveFunction = typeof resolveFn === 'function'
        const isRejectFunction = typeof rejectFn === 'function'

        function excuteFn(thenStatus) {
          return function(fn, newPromise) {
            const type = thenStatus === 'fulfilled' ? isResolveFunction : isRejectFunction
            try {
              newPromise.PromiseState = status = 'fulfilled'
              if (this.PromiseState === thenStatus && type) {
                if (fn) {
                  value = fn(this.PromiseResult)
                  newPromise.PromiseResult = value
                } else {
                  value = this.PromiseResult
                }
              }

              if (thenStatus === 'rejected' && !type) {
                const errorCallbacks = newPromise.errorCallbacks
                this.triggerErrorCallback(errorCallbacks)
              }
            } catch(err) {
              status = thenStatus
              value = err
              newPromise.PromiseState = status = 'fulfilled'
              setTimeout(() = > {
                this.triggerErrorCallback(newPromise.errorCallbacks)
              }, 0)}this.triggerFn(null, newPromise.finallyCallbacks)
          }
        }

        this.resolveFns.push(excuteFn('fulfilled').bind(this, resolveFn, this.newPromise))
        this.rejectFns.push(excuteFn('rejected').bind(this, rejectFn, this.newPromise))

        if(status ! = ='pending') {
          this.excuteThen()
        }

        promiseArray.push(this)
        return this.newPromise
      }

      catch(callback) {
        this.errorCallbacks.push(callback)

        return this.newPromise || (this.newPromise = new MyPromise())
      }

      finally(callback) {
        this.finallyCallbacks.push(callback)
   
        return this.newPromise || (this.newPromise = new MyPromise())
      }

      reconnect() {
        if (promiseArray.length) {
          const index = promiseArray.indexOf(this)
          if (~index) {
            promiseArray.slice(index).forEach(context= > {
              if (context instanceof MyPromise) {
                context.excuteThen()
              }
            })
          }
        }
      }

      triggerErrorCallback(errorCallbacks = this.errorCallbacks) {
        let currentFn = null
        while(currentFn = errorCallbacks.shift()) {
          if (currentFn) {
            currentFn(this.PromiseResult)
          }
        }
      }

      triggerFn(thenStatus, fn) {
        if(! fn) { fn = thenStatus ==='fulfilled' ? this.resolveFns : this.rejectFns
        }
        let currentFn = null
        while(currentFn = fn.shift()) {
          if (currentFn) {
            currentFn()
          }
        }
      }

      excuteThen() {
        setTimeout(() = > {
          if (this.PromiseState === 'fulfilled') this.triggerFn('fulfilled')
          else if (this.PromiseState === 'rejected') this.triggerFn('rejected')},10)}}Copy the code

The articles

🌲 mid-advanced front-end does not necessarily understand setTimeout | netease practice summary

80 lines of code to achieve Vue skeleton screen 🏆 | netease small practice

You misunderstood Vue nextTick