手写Promise过程中的“恍然大悟”

4,796 阅读13分钟

给我一篇文章的时间,带你了解手写Promise中的那些细节。

相信小伙伴们可能看到过许多次手写Promise的文章,但可能绝大部分是看到了面经里有“如何手写Promise”相关的问题才去了解它。笔者作为一名Promise已经相对成熟再入行的前端,也准备和大家一起探索下,该如何去手写出一个Promise。 ​

开始之前,让我们回忆一下,Promise具备了哪些能力,使得它能如此得常用。 先甩个最常用的场景,类似于这个🌰

const p=new MyPromise((resolve,reject)=>{
  setTimeout(()=>{
    resolve(1)
  },2000)  
})
p.then(v=>{
  console.log(v)
  return 2
})
.then(v=>{
  console.log(v)
})
// 我们期望能2s延迟后输出1,2


const p=new MyPromise((resolve,reject)=>{
  setTimeout(()=>{
    reject(1)
  },2000)  
})
p.then(v=>{
  console.log(v)
  return 2
},r=>{
  console.log(v)
  return 3
})
.then(v=>{
  console.log(v)
})
// 我们期望能2s延迟后输出1,3

相关的一些用法相比小伙伴们都很熟悉,就不再一一举例了,现在我们来结合下Promises/A+规范,列举下我们需要实现的大致功能。

特性

  1. 状态:必须是pending fulfilled rejected 之一,只能由pending转为fulfilledrejected且不可逆
  2. 需要提供一个then方法,
    1. 接受两个参数
    2. onFulfilled对应状态转为fulfilled时的回调;onRejected对应状态转为rejected时的回调
    3. 对同一个promise而言,then可以多次调用
    4. then需要返回一个promise
  3. 提供catch方法,catch(fn)结果与then(,fn)一致
  4. 静态方法 resolverejectallrace

根据前面列举的功能,我们开始针对例子进行简单的分析👇🏻

const p=new MyPromise((resolve,reject)=>{
  setTimeout(()=>{
    resolve(1)
  },1000)
})
p.then((v)=>{
  console.log(v)
},(rej)=>{
  console.error(rej)
})

本文常用名词声明

  1. fn 表示传入MyPromise构造函数的那个函数
  2. 成功回调 表示MyPromise由pending转为fulfilled时执行的回调
  3. 失败回调 表示MyPromise由pending转为rejected时执行的回调

例子中我们用setTimeout来做一个异步任务,实际业务场景中,这里会被替换成接口请求等。从js执行角度分析,这几行代码大概是这么几步:

  1. new了一下 MyPromise 这个构造函数,返回了一个对象p
  2. 构造函数接受一个函数fn作为参数,这个函数本身接受resolvereject两个参数,视具体业务情况调用resolve或者reject,且resolvereject都支持传参
  3. 返回的对象p上存在then方法,接受两个参数,成功回调和失败回调。类型都是函数,函数的入参就是构造函数入参中,resolvereject最终被调用时传入的值
  4. fn在间隔一段时间后,调用resolve(1),此时需要执行成功回调

恍然大悟 -- 1

分析到这一步,其实不难发现,fn中更改状态,接着执行回调函数,就是典型的发布订阅的模式。调用then方法的就是将成功或失败两种情况下的回调进行注册,具体执行哪一个由传入构造函数的函数执行结果所决定。而且,规范中也提到了一个promise实例,它的then可以调用多次,类似于可以被多次订阅,所以很容易想到用数组来管理这些回调。那么有了这个思路,我们就可以开始着手写一些代码了。 下载.jpeg

再结合下Promises/A+的规范,Promise只有pending, fulfilled, rejected三种状态,我们可以得出以下代码 因为涉及到new调用,所以这里我们直接使用class来处理MyPromise

const PENDING='pending'
const FULLFILLED='fullfilled'
const REJECTED='rejected'

class MyPromise{
  
  status=PENDING
  fullfilledStack=[]
  rejectedStack=[]
  
  constructor(fn){
    const resolve=(value)=>{
      for(const cb of this.fullfilledStack){
        cb(value)
      }
    }
    const reject=(reason)=>{
      for(const cb of this.rejectedStack){
        cb(reason)
      }
    }
    fn(resolve,reject)
  }

  then(res,rej){
    this.fullfilledStack.push(res)
    this.rejectedStack.push(rej)
  }
}

这是最基本的一个例子,对于回调的收集及调用,我们采用的方式和eventEmitter类似,跑了一下上方的🌰,没有问题,我们已经完成了我们🌰 中的场景,接着看。 ​

Promise的另一个特点,就是它链式调用的能力,也就是我们可以不断的then下去,而目前我们所写的代码,只支持then一次,那该如何解决这个问题呢? ​

首先,先确保能够连续的then下去,这一点,我们采用每次then都返回一个新的Promise实例来实现,then放方法相关代码如下:

then(res,rej){
  return new MyPromise((resolve,reject)=>{
    // 按不同条件,执行resolve或者reject
  })
}

由于返回的都是MyPromise的实例,所以连续调用then方法的问题解决了。接着需要我们解决的是,在多个then的情况下,如何保证每次then中的value都是上一个then方法的返回值。 结合我们在 恍然大悟 -- 1 中得出的逻辑,重新整理一下我们接下来要做的事。从分工上来看,fn 会被MyPromise的构造函数所调用,构造函数会为 fn 提供两个方法,供 fn 将所有在 then 上注册的回调进行调用。由此可得,fn 中我们需要显示去调用 resolve 或者 reject 。 当我们在then 方法中返回MyPromise实例时,此处的fn还需要把上一次成功回调的返回值拿到,并传给下一个resolve

恍然大悟 -- 2

在老的Promise状态更新后,会执行对应的回调,我们利用闭包的特性,把更改新的Promise的方法放在回调里就可以了!而且注册在老Promise上的回调的执行结果,我们只需要执行一下,就能够拿到返回值,传递给新的Promise的 resolve 方法或者 reject 方法了。

images (1).jpeg

结合这两点,我们可以得出👇🏻 这样的代码

then(res:Fn,rej?:Fn){
  return new MyPromise((resolve,reject)=>{
    const resCb=(v)=>{
      resolve(res(v))
    }
    this.fullfilledStack.push(resCb)

    const rejCb=(r)=>{
      reject(rej!(r))
    }
    this.rejectedStack.push(rejCb)
  })
}

将我们的🌰 改为链式调用,然后试一下我们新的then方法

const p=new MyPromise((res,rej)=>{
  setTimeout(()=>{
    res(1)
  },1000)
})
p
.then((v)=>{
  console.log(v)
  return 2
})
.then((v)=>{
  console.log(v)
  return 3
})
.then(v=>{
  console.log(v)
})

// 输出了 1、2、3

输出结果没有问题,但是笔者在写到这里时,意识到了一个问题,就是成功回调的数组,和失败回调的数组,它是单例的,还是跟着每个Promise实例的。在这里笔者分析了下代码执行时的this指向,虽然在then的内部都用到了箭头函数,但是then方法都是被每个Promise对象所调用的。

恍然大悟 -- 3

每次调用then,都会生成新的Promise实例,生成新实例时执行的那个fn函数,会往上一个Promise实例的回调数组里塞回调,所以其实是每个Promise实例都会维护自己的成功回调数组和失败回调数组,这也符合我们的预期。

images (3).jpeg

我们来看下现在代码的全貌:


class MyPromise{

  fulfilledStack=[]
  rejectedStack=[]

  constructor(fn){
    const resolve=(value)=>{
      for(const cb of this.fulfilledStack){
        cb(value)
      }
    }
    const reject=(reason)=>{
      for(const cb of this.rejectedStack){
        cb(reason)
      }
    }
    fn(resolve,reject)
  }

  then(onfulfilled,onrejected){
    return new MyPromise((resolve,reject)=>{
      const resCb=(v)=>{
        resolve(onfulfilled(v))
      }
      this.fulfilledStack.push(resCb)
      
      const rejCb=(r)=>{
        reject(onrejected(r))
      }
      this.rejectedStack.push(rejCb)
    })
  }
}

const p0=new MyPromise((resolve,reject)=>{
  setTimeout(()=>{
    resolve(1)
  },1000)
})

const p1 = p0.then((v)=>{ // cb1
  console.log(v)
  return 2
})
const p2 = p1.then((v)=>{ // cb2
  console.log(v)
  return 3
})
const p3 = p2.then((v)=>{ // cb3
  console.log(v)
})

到目前为止,我们已经支持了链式调用,再来一起看下MyPromise类中的contrustor和then两个方法的实现。 constructor:

  1. 接受fn一个函数作为参数
  2. 声明两个函数,分别负责将当前MyPromise实例上的成功、失败回调函数依次执行
  3. 将声明好的两个函数,作为参数传递给fn,供fn调用

then:

  1. 接受onfulfilled,onrejected两个函数作为参数
  2. 返回一个新的MyPromise实例
  3. 声明两个函数,负责值传递工作,并利用闭包,保有更改新的MyPromise状态的能力。并将onfulfilled()或onrejected()的返回值,传递新生成的MyPromise实例
  4. 将两个函数,分别push到当前MyPromise实例的数组中。最终达成的效果是:老MyPromise实例的回调,可以控制新MyPromise实例的状态。

我们再顺着我们的🌰 看看代码执行时,具体发生了什么。

  1. 在line 36执行时,返回了对象p0,我们注册了一个定时函数,它将在1s后,执行resolve(1)
  2. line 42,我们调用了对象p0的then方法,并传入了一个函数cb1。由then方法的实现可得,在这个then方法会返回一个新的对象p1,并且我们会往对象p0的 fulfilledStack 中push一个方法,这个方法中会去执行我们之前传入的cb1,并会将其返回值当做参数,传递给实例化p1时 fn 函数的 resolve 方法。
  3. line 46,我们在p1上调用了then方法,与line 42相同,我们会返回一个p2对象,并在p1的 fulfilledStack 中push一个方法,它会执行cb2,并将cb2的返回值传递给实例化p2对象的fn函数中的 resolve 方法。
  4. 后续的then调用不再赘述,当同步的方法执行完后,1s时间到,实例化p0所用的fn方法中的resolve(1)被执行,p0的 fulfilledStack 中的函数将以1作为参数依次执行。此时,cb1会正式执行,在控制到输出1,并将cb1的返回值2传递给p1的 resolve,p1的 fulfilledStack中的函数将以2作为参数依次执行。以此类推,最终控制台输出1,2,3

到目前为止,我们考虑的都是cb函数直接return值的场景,我们还需要考虑cb函数返回一个新的MyPromise实例的场景,例如下面的demo

const p=new MyPromise((resolve,reject)=>{
  setTimeout(()=>{
    resolve(1)
  },1000)
})
p
.then((v)=>{
  console.log(v)
  return new MyPromise((res)=>{
    setTimeout(() => {
      res(2)
    }, 1000);
  })
})
.then((v)=>{
  console.log(v)
  return 3
})
.then(v=>{
  console.log(v)
})

cb函数返回了MyPromise的实例,我们的then方法最终也会返回新的MyPromise实例,我们又该如何处理这种情况呢? ​

恍然大悟 -- 4

我们已经将then做成每次生成新的Promise实例并将新实例的状态与旧实例保持同步。现在既然cb返回的也是一个Promise实例,我们只需要把then新生成的Promise的resolve方法和reject方法,传递给cb返回的Promise实例的then就可以了。 下载 (1).jpeg 并且,我们似乎一直遗漏了方法执行报错的问题,这种场景需要将Promise置为rejected状态,我们补上这条链路

then(res,rej){
  return new MyPromise((resolve,reject)=>{
    const resCb=(v)=>{
      try{
        const result = res(v)
        if(result instanceof MyPromise){
          // res方法的return中,返回的是MyPromise实例 ,例如 return new MyPromise()
          // 将新构造出来的MyPromise实例的状态,交由res返回的MyPromise实例所控制
          result.then(resolve,reject)
        }else{
          resolve(result)
        }
      } catch (e){
        reject(e)
      }
    }
    this.fullfilledStack.push(resCb)

    const rejCb=(r)=>{
      try {
        const result = rej(r)
        if(result instanceof MyPromise){
          result.then(resolve,reject)
        }else{
          resolve(result)
        }
      } catch (e){
        reject(r)
      }
    }
    this.rejectedStack.push(rejCb)
  })
}

跑了下demo,输出正常,时间间隔也正确。至此,链式调用的问题已经被我们所解决了。 再把我们的目光放回Promises/A+规范,接下来我们需要处理的就是**值传递。**我们来看下规范中对于值传递的解释

 promise2 = promise1.then(onFulfilled, onRejected);
  1. If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1.
  2. If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.

我们针对这两种场景进行处理,不难得出如下代码

  then(res,rej){
    return new MyPromise((resolve,reject)=>{
      const resCb=(v)=>{
        if(typeof res !=='function'){
          return resolve(v)
        }
        try{
          const result = res(v)
          if(result instanceof MyPromise){
            // res方法的return中,返回的是MyPromise实例 ,例如 return new MyPromise()
            // 将新构造出来的MyPromise实例的状态,交由res返回的MyPromise实例所控制
            result.then(resolve,reject)
          }else{
            resolve(result)
          }
        } catch (e){
          reject(e)
        }
      }
      this.fullfilledStack.push(resCb)
      
      const rejCb=(r)=>{
        if(typeof rej !=='function'){
          return reject(r)
        }
        try {
          const result = rej!(r)
          if(result instanceof MyPromise){
            result.then(resolve,reject)
          }else{
            resolve(result)
          }
        } catch (e){
          reject(r)
        }
      }
      this.rejectedStack.push(rejCb)
    })
  }

到目前为止,我们一直在讨论Promise是如何处理异步的。代码执行顺序一直是,new Promise,通过then将回调进行绑定,再改变Promise状态,执行对应的回调,但如果我们的demo中没有那个setTimeout,那顺序就是new Promise,改变Promise状态,再注册回调,显然此时的回调是不会被执行的,所以,我们还需要调整我们的then方法,保证then执行前,Promise状态已经变更了,也能正确执行回调 ​


  then(res,rej){
    return new MyPromise((resolve,reject)=>{
      // ...
      this.fullfilledStack.push(resCb)
      this.status===FULLFILLED && resCb(this.value)
      
      // ...
      this.rejectedStack.push(rejCb)
      this.status===REJECTED && rejCb(this.value)
    })
  }
}


值传递和兼容同步都已经完成了,在这次的改动中,我们为MyPromise新增了value字段,用来将当前实例的值传递给对应的回调函数。 ​

到此为止,then方法的实现告一段落,我们再把成员方法catch以及resolve、reject、all、race等静态方法完善下。 在实现Promise.all时,遇到了同样的情况,就是该如何去获知是否所有Promise 都完成了呢? ​

恍然大悟 -- 5

我们可以利用一个简单的计数器,来判断是否每个Promise都已完成了

images (2).jpeg


  catch(cb:Fn){
    this.then(undefined,cb)
  }

  static resolve(val){
    if(val instanceof MyPromise) return val
    return new MyPromise(resolve=>{
      resolve(val)
    })
  }

  static reject(val){
    return new MyPromise((resolve,reject)=>{
      reject(val)
    })
  }

  static all(arr){
    return new MyPromise((resolve,reject)=>{
      var count=0
      var result=[]
      arr.forEach((p,index)=>{
        MyPromise.resolve(p).then((val)=>{
          count++
          result[index]=val
          if(count === arr.length){
            resolve(result)
          }
        },reason=>{
          reject(reason)
        })
      })
    })
  }
  
  static race(arr){
    return new MyPromise((resolve,reject)=>{
      for(const p of arr){
        MyPromise.resolve(p)
        .then(
          v=>resolve(v),
          r=>reject(r)
        )
      }
    })
  }

再通过用demo来验证我们的逻辑,发现在执行race时,多个MyPromise还是输出了多次结果,这不符合我们的预期,我们回过头去看看问题出在哪。 ​

恍然大悟 -- 6

我们给Promise增加了状态的概念,但没有限制状态之间的变更,导致Promise的状态可以变更多次,触发多次回调。

images.jpeg

现在我们加上这个限制,相关代码如下

constructor(fn){
  const resolve=(value:PValue)=>{
    if(this.status===PENDING){
      this.status=FULLFILLED
      this.value=value
      for(const cb of this.fullfilledStack){
        cb(value)
      }
    }
  }
  const reject=(reason)=>{
    if(this.status===PENDING){
      this.status=REJECTED
      this.value=reason
      for(const cb of this.rejectedStack){
        cb(reason)
      }
    }
  }
  fn(resolve,reject)
}

至此,我们已经完成了一个Promise的核心方法,经过一次次的“恍然大悟”,也完整的理解了手写一个Promise需要掌握的知识点 ​

完整代码


const PENDING='pending'
const FULLFILLED='fullfilled'
const REJECTED='rejected'

class MyPromise{

  status=PENDING
  value=undefined
  fullfilledStack=[]
  rejectedStack=[]

  constructor(fn){
    const resolve=(value)=>{
      if(this.status===PENDING){
        this.status=FULLFILLED
        this.value=value
        for(const cb of this.fullfilledStack){
          cb(value)
        }
      }
    }
    const reject=(reason)=>{
      if(this.status===PENDING){
        this.status=REJECTED
        this.value=reason
        for(const cb of this.rejectedStack){
          cb(reason)
        }
      }
    }
    fn(resolve,reject)
  }

  then(res,rej){
    return new MyPromise((resolve,reject)=>{
      const resCb=(v)=>{
        if(typeof res !=='function'){
          return resolve(v)
        }
        try{
          const result = res(v)
          if(result instanceof MyPromise){
            // res方法的return中,返回的是MyPromise实例 ,例如 return new MyPromise()
            // 将新构造出来的MyPromise实例的状态,交由res返回的MyPromise实例所控制
            result.then(resolve,reject)
          }else{
            resolve(result)
          }
        } catch (e){
          reject(e)
        }
      }
      this.fullfilledStack.push(resCb)
      this.status===FULLFILLED && resCb(this.value)
      
      const rejCb=(r)=>{
        if(typeof rej !=='function'){
          return reject(r)
        }
        try {
          const result = rej(r)
          if(result instanceof MyPromise){
            result.then(resolve,reject)
          }else{
            resolve(result)
          }
        } catch (e){
          reject(r)
        }
      }
      this.rejectedStack.push(rejCb)
      this.status===REJECTED && rejCb(this.value)
    })
  }

  catch(cb){
    this.then(undefined,cb)
  }

  static resolve(val){
    if(val instanceof MyPromise) return val
    return new MyPromise(resolve=>{
      resolve(val)
    })
  }

  static reject(val){
    return new MyPromise((resolve,reject)=>{
      reject(val)
    })
  }

  static all(arr){
    return new MyPromise((resolve,reject)=>{
      var count=0
      var result=[]
      arr.forEach((p,index)=>{
        MyPromise.resolve(p).then((val)=>{
          count++
          result[index]=val
          if(count === arr.length){
            resolve(result)
          }
        },reason=>{
          reject(reason)
        })
      })
    })
  }

  static race(arr){
    return new MyPromise((resolve,reject)=>{
      for(const p of arr){
        MyPromise.resolve(p)
        .then(
          v=>resolve(v),
          r=>reject(r)
        )
      }
    })
  }
}

参考文章

面试官:“你能手写一个 Promise 吗”

9k字 | Promise/async/Generator实现原理解析