Promise 分析及其源码实现

301 阅读11分钟

在工作过程中,我们总会利用 Promise 去实现部分功能,但总是知其然不知其所以然。因此,本篇文章主要探究 Promise 的实现原理,关于用法不过多赘述,就是一步一步实现一个 Promise


1. Promise基本结构


   const p1 = new Promise((resolve, reject) => {
     // ... some code
     if (/* 异步操作成功 */){
       resolve(value);
     } else {
        reject(error);
      }
   })

首先,Promise的构造函数接收一个回调函数作为参数。

其次,回调函数接收了两个参数,分别是resolve、reject,函数体中去选择性的执行resolve或者reject。

最后,Promise 实例生成以后,就要定义 resolve 状态、reject 状态分别需要执行的函数,这个操作需要在then方法中完成。

   p1.then(() => {
   	// success
   }, () => {
   	// failure
   })

当 resolve(value) 被执行,意味着状态从“未完成”变更为“成功“,then的第一个方法被执行,状态不变更则不执行;

当reject(error) 被执行,意味着状态从”未完成“变更未”失败“,then的第二个方法被执行,状态不变更则不执行;

那么Promise的基本结构就是以上,来分析一下它的实现逻辑。


实现逻辑

promise.png

  1. 定义Promise的三个状态,分别是未完成状态(pending)、成功状态(fulfilled)、失败状态(rejected);
  1. 需要创建一个类,类的构造函数接收一个函数(executor)作为参数,内部变量有:
  • status,存储Promise状态;
  • value,存储失败时的error或者成功时的value的变量;
  • resolveQueue,fulfilled 状态的函数变量,存储 then 方法传入的函数;
  • rejectQueue,rejected 状态的函数变量,存储 then 方法传入的函数;  
  1. 执行传入的函数executor,executor函数需要传入两个参数resolve、reject,那么必须在构造函数内有 resolve、reject 函数变量,在 executor 内部被执行,那么这两个函数需要完成以下内容:
  • 变更Promise状态;
  • 接收一个任意类型的值作为实参,表示Promise成功或者失败的值,并同步到value;
  • 执行 resolveQueue 或者 rejectQueue;  
  1. 需要有一个then方法,then 方法接收两个参数,一个是 fulfilled 时执行的函数,一个是 rejected 时执行的函数变量,then 方法内需要执行的操作是将这两个函数 变量分别存储到 resolveQueue、rejectQueue,等待状态变更后被执行

那么代码实现为:

   // 实现基本的then函数
   // 以及Promise 的状态实时变更
   // 实现一个Promise多次then的调用
   
   const PENDING = 'pending'
   const FULFILLED = 'fulfilled'
   const REJECTED = 'rejected'
   
   class ourPromise  {
     constructor(executor) {
       // this指向实例对象
       // 状态和返回值
       this.status = PENDING
       this.value = null
   
       // 回调函数存储
       this.resolveQueue = []
       this.rejectQueue = []
   
       // 成功时执行的函数
       let resolve = (val)=>{
          if(this.status !== PENDING) return;
          this.value = val
          this.status = FULFILLED
          this.resolveQueue.forEach(callback=>{
            callback(val)
          })
        }
	  // 失败时执行的函数
      let reject = (err) =>{
          if(this.status !== PENDING) return;
          this.value = err
          this.status = REJECTED
          this.rejectQueue.forEach(callback=>{
            callback(err)
          })
      }
   
       // 执行定义的实例传回来的回调函数,并传入两个参数
       executor(resolve,reject)
     }
   
     // then方法的两个参数是两个回调函数
     then(fulFilledFun,rejectedFun){
       this.resolveQueue.push(fulFilledFun)
       this.rejectQueue.push(rejectedFun)
     }
	}

 

测试一下

 const p1 = new ourPromise((resolve,reject)=>{
     setTimeout(()=>{
       resolve('success')
     },500)
   })
   
   p1.then((res)=>{
     console.log(res)
   })
   
	// 在500ms之后输出
	// success

 

那么基本的Promise就实现了,来梳理一下它的运行流程:

  1. 创建一个 ourPromise 实例,传入一个实参,是一个函数;
  1. 进入构造函数进行初始化,最后执行传入的实参函数,内部是异步函数,放入宏任务,继续执行同步语句;
  1. then方法被传入一个成功时执行的回调函数,那么此时,resolveQueue 变更,也就是说现在的 Promise 实例变更为
status = PENDING
value = null
resolveQueue = [
	(res)=>{
	  console.log(res)
	}
]
rejectQueue = []

可以看到,then并非是执行了什么语句,只是对当前实例的属性进行了改写。

  1. 同步任务执行完毕转去执行宏任务,执行到 resolve('success') ,以下函数被调用
let resolve = (val)=>{
      this.value = val
      this.status = FULFILLED
      this.resolveQueue.forEach(callback=>{
        callback(val)
      })
    }

为什么这里的then方法传入的两个函数要存储到一个数组内?

为了支持then 方法 可以被同一个Promise实例调用多次

比如:

const p1 = new ourPromise((resolve,reject)=>{
 setTimeout(()=>{
    resolve('success')
 },500)
})

p1.then((res)=>{
 console.log(res)
})
p1.then((res)=>{
  console.log('第二个then方法'+res)
})

// 500ms后输出
// success
// 第二个then方法success

 

2. then方法的实现

ES6 的 Promise 的 then 方法

p1.then(res,rej)

该then方法支持以下功能:

  1. res,rej 参数可选,但必须是函数
  • 当 p1 状态变为成功、失败时必须分别调用 res、rej,其参数为执行 resolve(value)、reject(err ) 时传入的 value、err
  • 在 p1 状态改变前其不可被调用
  • 调用次数不可超过一次
  1. then 方法可以被同一个 Promise 实例调用多次
  • 当 p1 成功状态时,所有 res 需按照其注册顺序依次回调
  • 当p1 失败状态时,所有 rej 需按照其注册顺序依次回调
  1. then 方法有返回值,返回值为 Promise 对象,支持链式调用

 

现在,我们来实现以上三个功能,其中第二点,在上面的 第1节 已经实现。那么先来实现 Promise 的核心的链式调用。

在学习 Promise 时,经常回遇到这样的例子,来测试 Promise 的 then 方法的链式调用。

   const p1 = new Promise(
     (resolve, reject) => {
        setTimeout(() => {
          resolve(1)
       }, 500);
     }
   )
   
   p1
     .then(res => {
         console.log(res)
         return 2
      })
     .then(res => {
         console.log(res)
         return 3
     })
     .then(res => {
         console.log(res)
     })
   
   // 顺序输出
   // 1
   // 2
   // 3

在这段代码可以明确的看到几点:

  1. p1.then() 仍然支持 then 方法,说明 p1.then() 也是一个 Promise 对象,即 then() 返回了一个 Promise 对象;
  1. p1.then() 方法既然返回了 Promise 对象,但成功的回调函数内部又没有写 resolve 语句,只写了 return 2。那么是在内部怎么处理的呢?p1.then() 返回的 Promise 对象又是什么时候变更状态的呢?

 

then 方法返回 Promise 实例

那么,先来是实现第一点,令其返回一个Promise对象,也就是来更改then方法。

then(fulFilledFun,rejectedFun){
    return new ourPromise((resolve,reject)=>{
      this.resolveQueue.push(fulFilledFun),
      this.rejectQueue.push(rejectedFun)
    })
  }

经过以上的改写,就可以成功的返回Promise,也可以支持链式的then方法且不会报错,但不能链式执行我们所写的函数。

基于此,各个Promise实例的注册时机如下图。总的来说,在 then 方法被调用时,不仅创建了新的Promise实例,也更改了上一实例的 resolveQueue,等待状态改变的时候被执行。其中,p1.then().then().then() 创建的Promise实例,它的resolveQueue实例未被更改,仍然是[]

1625747537680.png

 

链式执行 then 方法

现在来实现第二点,链式执行我们所写的then方法内的函数。换言之,是如何确保各个Promise实例的resolveQueue按顺序执行。

从链式执行的例子的结果来说,第一个 then() 方法的 return 内容,被第二个 then() 方法内函数拿到。经过上面的改写,实际上是:

  • p1 实例转为成功状态,执行成功状态时的回调函数 resolve(),resolve 内部执行 resolveQueue ,其长度为1;
  • p1 的 resolveQueue 被执行,执行完毕后立即执行 p1.then() 的成功的回调函数,以此类推;

那么,重点就是立即执行, 即前一个 resolveQueue 的执行 和后一个 状态更改为 fullfiled 两个时机是绑定在一起的。也就是需要重新包装 then() 方法传入的成功、失败的回调函数。

then()方法继续改写

  then(fulFilledFun,rejectedFun){
    // 首先返回一个 ourPromise 对象
    return new ourPromise((resolve,reject)=>{
      // 包装成功时得回调函数
      const resolveFun = value =>{

        // 执行回调函数,得到返回值 returnV
        let returnV = fulFilledFun(value)
        // 置 ourPromise 的状态为 fulfilled ,并且将返回值传递
        resolve(returnV)

      }
      // 包装失败时得回调函数
      const rejectFun = value =>{
        let returnV = rejectedFun(value)
        resolve(returnV)
      }
      
      // 更改实例的 resolveQueue
      this.resolveQueue.push(resolveFun)
      this.rejectQueue.push(rejectFun)
    })
  }

经过以上,就可以将开头的例子,成功打印出1,2,3了。注册流程上一小节已经阐述,来梳理注册以后的流程,如图所示。

1625813655149.png

经过改写之后,注册完成的各个实例的 resolveQueue 属性如图所示。现跟着图来梳理一下注册以后的运行流程:

  • resolve(1) 执行,执行p1实例的成功的回调函数;
  • 状态置为 fulfilled,执行对应resolveQueue,形参value 为 1,执行对应 fulFilledFun,打印1,返回值为 2;
  • 执行到 resolve(2),执行p1.then() 实例的成功的回调函数;
  • 状态置为 fulfilled,执行对应resolveQueue,形参value 为 2,执行对应 fulFilledFun,打印2,返回值为 3;
  • 执行到 resolve(3),执行p1.then().then() 实例的成功的回调函数;
  • 状态置为 fulfilled,执行对应resolveQueue,形参value 为 3,执行对应 fulFilledFun,打印3,返回undefined;
  • 执行 resolve(undefined),状态置为 fulfilled,对应resolveQueue为空,结束。

这样简单的链式调用就基本实现了,现在来考虑一些细节问题。

 

then 细节实现

  1. then 方法接收的参数可能不是函数,要进行错误处理,并且不影响下一个then方法的执行,即值穿透;
const p1 = new Promise(
     (resolve, reject) => {
        setTimeout(() => {
          resolve(1)
       }, 500);
     }
   )
   
   p1
     .then()
     .then(res => {
         console.log(res)
         return 3
     })
     .then(res => {
         console.log(res)
     })

   // 顺序输出
   // 1
   // 3

 

  1. 成功、失败状态下的回调函数,可能返回一个 Promise 对象;
const p1 = new Promise(
     (resolve, reject) => {
        setTimeout(() => {
          resolve(1)
       }, 500);
     }
   )
   
   p1
     .then(res => {
       console.log(res)
       return new Promise((resolve, reject) => {
            setTimeout(() => {
              resolve(2)
           }, 500))
     })
     .then(res => {
         console.log(res)
         return 3
     })
     .then(res => {
         console.log(res)
     })
       
   // 顺序输出
   // 1
   // 3

 

  1. Promise 实例的executor函数,是一个同步任务;
 const p1 = new Promise(
     (resolve, reject) => {
        // 同步任务,所有代码顺序执行
        resolve(1)
     }
   )
   
   p1
     .then(res => {
         console.log(res)
         return 2
      })
     .then(res => {
         console.log(res)
         return 3
     })
     .then(res => {
         console.log(res)
     })

   // 顺序输出
   // 1
   // 2
   // 3

 

  1. 需要支持 Promise.resolve(x)、Promise.reject(x),且返回一个Promise实例,状态直接是fulfilled、rejected;
Promise.resolve(1).then(
	res=>{
        console.log(res)
    }
)
// 输出 1
Promise.resolve(
  new ourPromise((resolve,reject)=>{
    resolve('2')
  })
)
ourPromise.reject(1)

 

代码如下:

then(fulFilledFun,rejectedFun){
    // 第一个问题,接收参数不是函数时,构造一个新函数
    // 成功的回调函数,需要完成值的传递任务,需要让下一个 then 方法获得上一个 then 方法的value
    typeof fulFilledFun !== 'function' ? fulFilledFun = value => value :null
    // 失败的回调函数,需要完成将错误原因输出,并结束
    typeof rejectedFun !== 'function' ? rejectedFun = reason =>{
      throw reason
    } :null
    return new ourPromise((resolve,reject)=>{
      const resolveFun = value =>{
        let returnV = fulFilledFun(value)
        // 第二个问题,得到返回值后进行分类讨论
        // 如果是 Promise 对象,就执行该对象的 then 方法,并绑定 当前Promise对象的resolve、reject
        returnV instanceof ourPromise 
          ? returnV.then(resolve,reject)
          :resolve(returnV)
      }
      const rejectFun = value =>{
        let returnV = rejectedFun(value)
        returnV instanceof ourPromise 
          ? returnV.then(resolve,reject)
          :resolve(returnV)
      }
		
      // 第三个问题,如果是同步的情况,判断当前状态
      // PENDING,则依次将函数放入队列
      // FULFILLED,即执行到then方法时,已经执行了resolve,那么直接执行resolveFun
      // REJECTED,即执行到then方法时,已经执行了reject,那么直接执行rejectFun
      switch(this.status){
        case PENDING:
          this.resolveQueue.push(resolveFun)
          this.rejectQueue.push(rejectFun)
          break
        case FULFILLED:
          resolveFun(this.value)
          break
        case REJECTED:
          rejectFun(this.value)
      }
    })
  }

  // 第四个问题,静态的resolve、reject方法
  static resolve(value){
    if(value instanceof ourPromise) return value
    return new ourPromise(resolve => resolve(value))
  }

  static reject(value){
    return new ourPromise((resolve,reject) => reject(value))
  }

then 方法的错误捕获

前面的部分,我们没有注意到如果是函数内部出错,比如

const p1 = new ourPromise((resolve, reject) => {
    throw new Error('执行器错误')
})
 
p1.then(res => {
  console.log(res)
}, err => {
  console.log(err)
  console.log(err.message)
})


// 不会被打印
console.log('aaaa')

可以看到是executor抛出错误,那么

constructor(excutor) {
    // ...

    // 执行定义的实例传回来的回调函数,并传入两个参数
    // excutor(resolve,reject)
    try{
       excutor(resolve,reject)
     }catch(error){
       reject(error)
     }
  }

还有可能是,then方法内传入的成功、失败的两个回调函数内部抛出错误,同上,进行改写then方法

const resolveFun = value =>{
          try{
            let returnV = fulFilledFun(value)
            returnV instanceof ourPromise 
              ? returnV.then(resolve,reject)
              :resolve(returnV)
          }catch(error){
            reject(error)
          }
      }
const rejectFun = value =>{
          try{
            let returnV = rejectedFun(value)
            returnV instanceof ourPromise 
              ? returnV.then(resolve,reject)
              :resolve(returnV)
          }catch(error){
            reject(error)
          }
      }

这样写,当前fulFilledFun、rejectedFun内部如果抛出错误,会被下一个then方法的reject捕捉到。

then 方法的微任务处理后完整代码

很多手写版本都是使用 setTimeout 去做异步处理,但是 setTimeout 属于宏任务,这与 Promise 是个微任务相矛盾,所以我打算选择一种创建微任务的方式去实现我们的手写代码。

这里我们有几种选择,一种就是 Promise A+ 规范中也提到的,process.nextTick( Node 端 ) 与MutationObserver( 浏览器端 ),考虑到利用这两种方式需要做环境判断,所以在这里我们就推荐另外一种创建微任务的方式 queueMicrotask

// 实现基本的then函数
// 以及promise 的状态实时变更
// 实现一个promise多次then的调用

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class ourPromise  {
  constructor(excutor) {
    // this指向实例对象
    // 状态和返回值
    this.status = PENDING
    this.value = null

    // 函数存储
    this.resolveQueue = []
    this.rejectQueue = []

    let resolve = (val)=>{
      if(this.status !== PENDING) return;
      this.value = val
      this.status = FULFILLED
      this.resolveQueue.forEach(callback=>{
        callback(val)
      })
    }

    let reject = (err) =>{
      if(this.status !== PENDING) return;
      this.value = err
      this.status = REJECTED
      this.rejectQueue.forEach(callback=>{
        callback(err)
      })
    }

    // 执行定义的实例传回来的回调函数,并传入两个参数
    try{
      excutor(resolve,reject)
    }catch(error){
      reject(error)
    }
  }

  // then方法的两个参数是两个回调函数
  then(fulFilledFun,rejectedFun){
    // 判断当前函数是否为函数
    typeof fulFilledFun !== 'function' ? fulFilledFun = value => value :null
    typeof rejectedFun !== 'function' ? rejectedFun = reason =>{
      throw reason
    } :null
    return new ourPromise((resolve,reject)=>{
      const resolveFun = value =>{
        // 创建一个微任务等待 Promise 初始化
        queueMicrotask (()=>{
          try{
            let returnV = fulFilledFun(value)
            returnV instanceof ourPromise 
              ? returnV.then(resolve,reject)
              :resolve(returnV)
          }catch(error){
            reject(error)
          }
        })
      }
      const rejectFun = value =>{
        queueMicrotask(()=>{
          try{
            let returnV = rejectedFun(value)
            returnV instanceof ourPromise 
              ? returnV.then(resolve,reject)
              :resolve(returnV)
          }catch(error){
            reject(error)
          }
        })
      }

      switch(this.status){
        case PENDING:
          this.resolveQueue.push(resolveFun)
          this.rejectQueue.push(rejectFun)
          break
        case FULFILLED:
          resolveFun(this.value)
          break
        case REJECTED:
          rejectFun(this.value)
      }
    })
  }

  static resolve(value){
    if(value instanceof ourPromise) return value
    return new ourPromise(resolve => resolve(value))
  }

  static reject(value){
    return new ourPromise((resolve,reject) => reject(value))
  }
}