面试高频题目-手写Promise(内附完整代码) | 网易小实践

591 阅读9分钟

面试高频 Promise

Promise出现的背景

  1. 异步回调函数不连续,多个回调函数同时注册,执行时间却不够线性
  const callback1 = function() {}
  const callback2 = function() {}
  const callback3 = function() {}
  
  const res1 = ajax(callback1)
  const res2 = ajax(callback2)
  const res3 = ajax(callback3)

三个回调函数同时注册,但是他们的调用时机确实不固定的

  1. 回调地狱
  function triggerCb(cb1) {
    return function(cb2) {
         cb1()
         cb2()
    }
  }

回调地狱会导致以下几个问题:

1.容易造成代码的可读性极差,并且内部的操作会非常紊乱

2.每个回调函数都有成功和失败的状态,需要每个回调函数都维护一套错误机制

熟悉设计模式的小伙伴应该清楚职责链模式, Promise属于职责链中异步调用的典型案例

Promise知识点汇集

Promise的状态

Promise的状态分为: pendingfulfilledrejected

根据官方文档:

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.

翻译出中文就是说当状态从pending变成fulfiiled或者rejected后就已经锁定,无法回溯以及变更

Promise的thenable

Promise.then的用法想必大家不陌生,但是具体它里面有哪些隐含点,今天可以梳理下

1.then的参数不一定是回调函数,并且透传值

then的两个参数本身定义是resolvedFn和rejectedFn,但是它是可以传非函数形式的数据

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

以上例子最终执行结果res是1,我们可以发现当then处理了非函数形式的数据时,会把原来正确的PromiseResult直接传递给了下一个then(包括rejectedFn也是如此)

2. then返回的是一个新的Promise then返回的变量其实已经和调用then方法的promise不是一个promise了

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

以上例子中, p1是p调用then方法之后返回的变量,执行p1 !== p,结果是true,证明了then返回的不是一个promise

then must return a promise;

以上翻译就是将Promise.then回调函数必须是一个promise

3. then可以被一个Promise多次调用

上面说到then返回的是一个新的Promise,我们编写代码的时候通常都是链式调用,继承上一个Promise生成一个新的Promise,但是我们也可以使用变量缓存Promise,然后在该变量上注册若干个then回调函数

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

这两个then方法注册在p上,所以p的状态会同时作用于这两个then方法上

4. then返回的状态

我们知道then方法返回的是一个新的Promise之后,但是它的状态是否会发生变化呢? 绝大部份情况下then返回的Promise都是fulfilled状态,但是只要then回调函数里抛出错误就会将新的Promise状态置为rejected

  var p = Promise.reject('martin')
  
  var p1 = p.then(() => {}) // 正常情况下是fulfilled
  var p2 = p.then(() => { throw new Error('martin') }) // 此时就会变成rejected
  var p3 = p.then(() => { return new Error('martin') }) // 只有throw才会变成rejected,此时还是fulfilled

Promise.catch

1. Promise.catch会rejected状态下的Promise

上面我们说到过多个then可以作用于同个promise,那么catch同样也可以

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

上面then的第二个回调函数reject和下面的两个catch都会执行,不知道大家此处是否会有疑问,因为之前有个逻辑其实是说当reject函数注册的时候catch不会执行,其实说的不太准确,then返回的是一个新的Promise,当发生错误的时候,如果上游的Promise没有拦截掉那么就会由下游的Promise进行兜底

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

上面的代码执行后可以发现p2的catch回调函数执行而p3却没有,这是因为p3在注册catch的时候会被之前then注册的reject函数拦截掉

2. Promise.catch会劫持then回调函数发生错误的情况下

此处就不说了,then发生的错误就会被后面的catch劫持掉

3. Promise.catch返回的是Promise状态 Promise.catch返回的Promise状态判断与then一样

Promise.finally

1. Promise.finally状态跟随上一个Promise

Promise.finally的状态会跟随上一个Promise

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

p1的状态会跟随这p的状态,所以此时是rejected

手写Promise

  1. 我们首先定义一个Promise的一个类,它有着状态、值两个私有值
  class AWeSomePromise {
    constructor() {
      this.PromiseState = 'pending'
      this.PromiseResult = void 0
    }
  }
  1. Promise实例化时传入的是一个回调函数,回调函数的两个参数分别是resolve与reject函数,resolve和reject函数主要做的是将值赋值为传入的值并且更改状态
  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 // 赋值
      this.PromiseState = promiseState // 变更状态
      
      triggerFn() // 触发thenable,具体实现看下面
    }
  }
  1. 在原型上定义一个then方法,在这里需要结合上面Promise.then说到的几个特性

    • thenable返回的是一个新的Promise
      then(resolveFn, rejectFn) {
        this.newPromise = new AWeSomePromise() // then返回的是一个新的Promise
        
        return this.newPromise
      }
    
    • 第二个及后面thenable会根据之前的状态直接调用resolveFn或者rejectFn
        then() {
          if (status !== 'pending') {
            this.excuteThen()
          }
        }
        
        excuteThen() {
          setTimeout(() => {
            if (this.PromiseState === 'fulfilled') this.triggerFn('fulfilled')
            else if (this.PromiseState === 'rejected') this.triggerFn('rejected')
           }, 10)
        }
    
    • thenable接收的参数可以是非函数的数据,并且若干个thenable可以同时作用于一个Promise实例上
      get resolveFns() { return [] } // 保存resolveFn
      get rejectFns() { return [] } // 同理
      then(resolveFn, rejectFn) {
          const isResolveFn = typeof resolveFn === 'function' // 判断reslveFn是否是函数
          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) { // 判断传入的resolveFn是否是函数形式
                    value = fn(this.PromiseResult) // value是全局的值,等下会讲到
                    if (typeof value === 'undefined') value = this.PromiseResult // 如果thenable里没有返回值,则会对值进行透传
                    newPromise.PromiseResult = value // 更新下一个Promise的值(当Promise是异步的时候)
                  } else {
                    value = this.PromiseResult
                  }
                }
                if (thenStatus === 'rejected' && !type) {
                  const errorCallbacks = newPromise.errorCallbacks
                  this.triggerErrorCallback(errorCallbacks) // 触发catch回调函数
                }
              } catch(err) {
                status = thenStatus
                value = err
                newPromise.PromiseState = status = 'fulfilled'
                setTimeout(() => {
                  this.triggerErrorCallback(newPromise.errorCallbacks)
                }, 0)
              }
    
              this.triggerFn(null, newPromise.finallyCallbacks) // 触发finally回调函数
            }
          }
        this.resolveFns.push(excuteFn('fulfilled').bind(this, resolveFn, this.newPromise))
        this.rejectFns.push(excuteFn('rejected').bind(this, rejectFn, this.newPromise))
      }
      
      triggerFn(thenStatus, fn) { // 循环触发resolveFns和rejectFns
        if (!fn) {
          fn = thenStatus === 'fulfilled' ? this.resolveFns : this.rejectFns
        }
        let currentFn = null
        while(currentFn = fn.shift()) { // 这里需要从resolveFns中取出最前面进入的回调函数执行并且踢出队列
          if (currentFn) {
            currentFn()
          }
        }
      }
      
      // 此时需要更改下上一步resolve、reject、excuteStatus的操作
      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(() => { // 因为Promise.thenable是微任务,所以这里用setTimeout来模拟
          if (cb) {
            cb()
          }
          
          this.triggerFn(this.finallyCallbacks) // 触发finallyCallbacks
        }, 0)
      }
    
    • thenable状态和值透传 我们知道第二次及后面注册的thenable的状态不是由resolve和reject函数来更改的,所以这里需要在全局环境下定义几个值用于缓存上一个的状态
        let status = 'pending'
        let value = void 0 
        
        class AWeSomePromise {
           constructor(callback) {
             this.PromiseState = status // 这里直接先赋值上一次的状态
             this.PromiseResult = value // 这里直接先变更上一次的值
    
             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) { // 判断传入的resolveFn是否是函数形式
                        value = fn(this.PromiseResult) // value是全局的值, 这里对上一个thenable返回的值进行缓存
                        if (typeof value === 'undefined') value = this.PromiseResult // 如果thenable里没有返回值,则会对值进行透传
                        newPromise.PromiseResult = value // 更新下一个Promise的值(当Promise是异步的时候)
                      } else {
                        value = this.PromiseResult
                      }
                    }
                  } catch(err) {
                    status = thenStatus
                    value = err
                  }
                }
                ...
              }
           }
        }
    

    这里可能有点绕,我们理清下newPromise和Promise的关系

      var p = new AWeSomePromise()
      
      var p1 = p.then()
      p.newPromise === p1
    

    关系如下: Promise.newPromise === nextPromise 这样子thenable的基本原理已经开发完成,接下来看下catch函数的问题

  2. catch函数它与thenable开发流程很相似,我们也可以根据它的特性来开发, 若干个catch可以同时作用于一个Promise实例, catch函数劫持thenable的错误以及reject函数未注册时触发rejected状态的Promise

      reject() {
        this.excuteStatus(rejectvalue, 'rejected', () => {
          this.triggerErrorCallback() // 这里需要触发已经注册在当前Promise下的所有catch回调函数
          this.triggerFn.bind(this, 'rejected')
        })
      }
      get errorCallbacks() { return [] }
      catch(callback) {
        this.errorCallbacks.push(callback)
        
        return this.newPromise || (this.newPromise = new AWeSomePromise()) // catch之前可能没有注册thenable,所以需要处理下this.newPromise
      }
      
      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函数没有劫持的情况下会去调用catch函数
                const errorCallbacks = newPromise.errorCallbacks
                this.triggerErrorCallback(errorCallbacks)
              }
             } catch(err) {
               newPromise.PromiseState = status = 'fulfilled'
               setTimeout(() => {
                this.triggerErrorCallback(newPromise.errorCallbacks)
               }, 0)
             }
           }
         }
      }
    
  3. finally函数无论resolve函数触发还是reject函数触发最终都会执行

    excuteStatus(cb) {
      setTimeout(() => {
        if (cb) {
          cb()
        }
         
        this.triggerFn(this.finallyCallbacks) // 这里是为了触发当前Promise下finally回调函数 promise.finally()
      }, 0)
    }
    then() {
      function excuteFn() {
        return function() {
          ...
          this.triggerFn(null, newPromise.finallyCallbacks) // 这里触发的是thenable后链式注册的回调函数 then().finally
        }
      }
    }
  1. 处理异步, 这也是Promise的核心案例, 我们可以先看下异步的例子:
  var p = new Promise(resolve => {
    setTimeout(() => { resolve('martin') }, 1000)
  })
  
  p.then(() => {}).then(() => {})...

以上p.thenable函数及后面所有链式调用的thenable函数会在1000ms延迟后才会执行,根据我们上面写的第二个及后面的thenable函数在注册调用时就会去触发excuteThen函数,延迟10ms后会去触发resolveFns,但是以上案例明显需要再次去延迟1000ms之后才能触发,所以这里虽然我们不知道resolve函数会在延迟几秒之后才会执行,但是它执行的时候就代表可以去执行后续的thenable的回调函数,所以我们只需将thenable返回的Promise实例缓存到数组里,在resolve函数执行完后去检测当前promise的实例在数组中的下标值,并将后续添加进来的promise逐个触发resolveFns即可

  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()
             }
           })
         }
       }
     }
  }

这样就可以初步的完成Promise的模拟

完整代码

    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)
      }
    }

往期文章

🌲中高级前端不一定了解的setTimeout | 网易实践小总结

80行代码实现Vue骨架屏🏆 | 网易小实践

你理解错误的Vue nextTick