轻松实现链式调用Then, Catch的简易Promise

1,051 阅读6分钟

轻松实现链式调用Then, Catch的简易Promise

再过不久,我也要投身到求职的浪潮之中,一直听说今年行情太差,自己所在的公司也就我一个前端, 一年经验还没人带,实在太难受。为了不被拍死在沙滩上,只好再刷刷面试题。 看到一些手写代码的文章中有实现Promise的题目,自己也试着写了写,在这儿做个笔记,加深加深印象。 如有错误还请指正。

实现特性

实现Promise之前并没有系统看过A+规范,特性都是自己在控制台用原生Promise实验的,不对的地方还请指正。

  • 状态转换(pending| fulfilled(resolved) | rejected)
  • then 方法返回当前Promise
  • 支持then,catch 方法链式调用,手动再次调用
  • then 中可返回Promise对象
  • then 带有成功或者失败回调参数then(success, failed)
  • 保证then和catch顺序
  • 捕获运行时错误

思路

看了几篇面试文,里面实现的Promise功能都不齐全,而且稍显复杂,所幸拜读过晨曦时梦见兮大佬的最简实现Promise, 里面很巧妙的实现了then的链式调用。20行代码就完成了一个带有then函数的Promise。感兴趣的同学可以多去看看。

回顾Promise用法,我们在Promise构造函数中传入一个可能带有异步操作的函数。 对创建的Promise对象链式调用then方法,每一次的then的第一个回调函数的参数是上一次的返回值。

  1. 我们将Promise看作是一次装弹射击的动作。
  2. 每次.then(callback)就相当于是填充一颗子弹,用一个弹夹(数组)存储起来。
  3. 当弹药填充完毕时(也就是then收集完成时),依次射击(执行then回调)
  4. 当有下一轮射击时(再次手动添加then),重复2步骤。

如何得知then收集完毕呢?晨曦时梦见兮大佬将执行then回调的函数设计为setTimeout, 链式调用then就是普通代码。

new Promise(setTimeout)
    .then(cb1)
    .then(cb2)

其中setTimeout作为宏任务,会在then函数(收集回调,并没有执行回调)同步调用之后再执行,这就解决的监听then收集完成的问题。

实现

初始状态

const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
function MyPromise (executor) {
    const self = this
    self.__CallBacks__ = []            // then 方法回调队列
    self.__PromiseValue__ = undefined
    self.__PromiseStatus__ = PENDING   // 初始化状态
    self.__resolve__ = function () {   // 成功回调
        // do something here .
    }
    executor(self.__resolve__, self.__reject__)
}

实现then函数

__CallBacks__就是弹夹,then传入的callback就是子弹,then当然就充当填充子弹的动作。

MyPromise.prototype.then = function ( callback ) {
  this.__CallBacks__.push( callback )
  return this // 返回当前promise对象
}

执行回调

__resolve__作为执行回调的方法,我们将它用setTimeout包裹

self.__resolve__ = function ( lastValue ) {  //  类似于开始射击的动作
  // 因为我们要先填充弹药(记录所有then回调)再射击
  // 所以射击需要在最后执行  这里考虑使用setTimeout
  setTimeout( () => {
    if ( self.__PromiseStatus__ === PENDING ) self.__PromiseStatus__ = RESOLVED 
    self.__PromiseValue__ = lastValue
    
    const nextCallBack = self.__CallBacks__.shift() // 每次射击(执行)时取出一颗子弹(回调)
    if ( nextCallBack ) {
      const param = nextCallBack( lastValue ) // 得到上一次执行的结果
      ///////////////////////////////////////////////////////////////////////////////
      //  若回调返回的是Promise(内部Promise),我们应该等待其执行结束再执行外部逻辑   //
      ///////////////////////////////////////////////////////////////////////////////
      param instanceof MyPromise
        ? param.then( self.__resolve__ )
        : self.__resolve__( param )
    }
  } )
}

__resolve__中首次调用改变__PromiseStatus__状态。一旦修改后续不会改变。 然后shift出一个回调,执行该回调,拿到结果。

param.then能拿到内部Promise最后一次执行的结果反馈。

这里考虑若then中返回的是一个Promise对象,应该等待内部Promise完全执行完毕之后, 再执行自身的__resolve__,保证执行顺序正确。否则执行下一次__resolve__(也就是下一个回调)。

重新装弹

肯定会有同学在调试的时候发现,已经射击(resolve)一次后,后续继续添加子弹(then回调)都不会执行了。

var first = new MyPromise((resolve) => resolve(666))
              .then(v => {
                console.log('first then', v)
                return 233
              })
// 控制台打印出first then后,手动在控制台重新输入
first.then(v => console.log('second then', v))  

second then没有被打印出来,这是因为上一次在构造函数中, 延迟执行了射击动作 resolve + setTimeout。 如果再重新填充弹药,就不可能在执行一次初始化,也就不能射击了。 所以我们需要在下一轮弹药填充(then)时重新执行延迟射击。

MyPromise.prototype.then = function ( successCB ) {
  // 当上一次射击结束时(上一次填充的弹药消耗完毕) 延迟执行下一次回调
  if ( this.__PromiseStatus__ !== PENDING && this.__CallBacks__.length === 0) {
    this.__resolve__(this.__PromiseValue__)
  } 
  this.__CallBacks__.push( successCB )
  return this // 返回当前promise对象
}

至此,一个没有异常捕获的简易Promise算是完成了。这个Promise可以链式调用,重新调用。 完整代码可在文末查看

实现catch

catch只会捕获它之前的reject回调或者异常抛出,一旦catch后,catch之前的then都会失效,也就是说catchthen是有顺序的。 所以我还是使用__CallBacks__来存储catch回调,为了区分,__CallBacks__存储的是一个对象{callback, type} type 是区分catchthen 的标识。

MyPromise.prototype.then = function ( successCB, rejectCB ) {
  if ( callback instanceof Function ) {
    if ( this.__PromiseStatus__ !== PENDING && !this.__CallBacks__.some(t => t.type === 'then') ) this.__resolve__( this.__PromiseValue__ )
    this.__CallBacks__.push( { callback: successCB, type: 'then' } )
  }

  if ( rejectCB instanceof Function ) {
    this.__CallBacks__.push( { callback: rejectCB, type: 'catch' } )
  }
  return this
}

MyPromise.prototype.catch = function ( callback ) {
  return this.then( undefined, callback )
}

可能有些同学不知道,then函数第二个参数就是catch回调,我们在then中实现,catch直接调用。

这里注意下,我们使用type区分回调后,想要再次执行回调时,不是判断__CallBacks__是否为空,而是判断__CallBacks__里面是否then为空

重写__resolve__以适配catch

__resolve__函数添加第二个参数isReject标识当前处理的目标是否为catch回调

self.__resolve__ = function ( lastValue, isReject = false ) {
  setTimeout( () => {
    if ( self.__PromiseStatus__ === PENDING ) self.__PromiseStatus__ = isReject ? REJECTED : RESOLVED
    // 每次调用更新Promise值
    self.__PromiseValue__ = lastValue 
    const cbType = isReject ? 'catch' : 'then'
    let item
    // 循环队列 直到找到第一个匹配的回调
    while ( item = self.__CallBacks__.shift() ) {
      if ( item && item.type === cbType) {
          const param = item.callback( lastValue )
          param instanceof MyPromise ? param.then( self.__resolve__, self.__reject__) : self.__resolve__( param )
          break
      }
    }
  } )
}
self.__reject__ = ( val ) => self.__resolve__( val, true )
executor( self.__resolve__, self.__reject__ )

上述代码也比较好理解,我们用一个while循环匹配当前目标回调类型cbType, 找到之后就执行。 如果这里没匹配到目标函数,之前shift推出的函数都会被丢弃,所以catch之前的then都不会生效。

如果then回调抛出了异常呢?

这也比较简单,我们将执行回调函数的地方用try catch包裹,一旦捕获到错误,调用__reject__执行用户逻辑

if ( item && item.type === cbType) {
  try {
    const param = item.callback( lastValue )
    param instanceof MyPromise ? param.then( self.__resolve__, self.__reject__) : self.__resolve__( param )
  } catch (e) { 
      self.__reject__( e ) 
  }
  break
}

出现了错误 但是没有catch函数怎么办?

当抛出错误后,程序会循环匹配catch回调,此时我们的代码会将__CallBacks__清空(因为全都是then回调,找不到catch), 此时再手动抛出错误就行

self.__resolve__ = function ( lastValue, isReject = false ) {
  setTimeout( () => {
    if ( self.__PromiseStatus__ === PENDING ) self.__PromiseStatus__ = isReject ? REJECTED : RESOLVED
    self.__PromiseValue__ = lastValue 
    // 当失败/异常,并且没有回调(catch)时,手动抛出异常
    if ( isReject && self.__CallBacks__.length === 0 ) throw new Error( lastValue )
    const cbType = isReject ? 'catch' : 'then'
    // ......
  } )
}

代码示例

简易实现(then) 30行代码

const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
function MyPromise (executor) {
    const self = this
    self.__CallBacks__ = []  
    self.__PromiseValue__ = undefined
    self.__PromiseStatus__ = PENDING  
    self.__resolve__ = function ( lastValue ) { 
      setTimeout( () => {
        if ( self.__PromiseStatus__ === PENDING ) self.__PromiseStatus__ = RESOLVED 
        self.__PromiseValue__ = lastValue
        const nextCallBack = self.__CallBacks__.shift() 
        if ( nextCallBack ) {
          const param = nextCallBack( lastValue ) 
          param instanceof MyPromise ? param.then( self.__resolve__ ) : self.__resolve__( param )
        }
      } )
    }
    executor(self.__resolve__)
}

MyPromise.prototype.then = function ( successCB ) {
  if ( this.__PromiseStatus__ !== PENDING && this.__CallBacks__.length === 0) {
    this.__resolve__(this.__PromiseValue__)
  } 
  this.__CallBacks__.push( successCB )
  return this 
}

进阶实现(then,catch) 50行代码

const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";

function MyPromise ( executor ) {
  const self = this
  self.__CallBacks__ = []
  self.__PromiseValue__ = undefined
  self.__PromiseStatus__ = PENDING

  self.__resolve__ = function ( lastValue, isReject = false ) {
    setTimeout( () => {
      if ( self.__PromiseStatus__ === PENDING ) self.__PromiseStatus__ = isReject ? REJECTED : RESOLVED
      self.__PromiseValue__ = lastValue 
      if ( isReject && self.__CallBacks__.length === 0 ) throw new Error( lastValue )
      const cbType = isReject ? 'catch' : 'then'
      let item
      while ( item = self.__CallBacks__.shift() ) {
        if ( item && item.type === cbType) {
          try {
            const param = item.callback( lastValue )
            param instanceof MyPromise ? param.then( self.__resolve__, self.__reject__) : self.__resolve__( param )
          } catch (e) { 
              self.__reject__( e ) 
          }
          break
        }
      }
    } )
  }

  self.__reject__ = ( val ) => self.__resolve__( val, true )
  executor( self.__resolve__, self.__reject__ )
}

MyPromise.prototype.then = function ( callback, rejectCallback ) {
  if ( callback instanceof Function ) {
    if ( this.__PromiseStatus__ !== PENDING && !this.__CallBacks__.some(t => t.type === 'then') ) 
        this.__resolve__( this.__PromiseValue__ ) 
    this.__CallBacks__.push( { callback, type: 'then' } )
  }

  if ( rejectCallback instanceof Function ) {
    this.__CallBacks__.push( { callback: rejectCallback, type: 'catch' } )
  }
  return this
}

MyPromise.prototype.catch = function ( callback ) {
  return this.then( undefined, callback )
}

本文使用 mdnice 排版