Promise 系列 | 进阶篇 - 手写 Promise 特别行动

271 阅读14分钟

我正在参加「掘金·启航计划」

前言

在前两篇文章:开篇 - 你对 Promise 的基本特性了解多少基础篇 - 3 个对象方法和 6 个类方法 里我们已经清楚了 Promise 的基本特性和其方法的使用 。

这节我们就来实现一个简单的 Promise,这不仅能帮助我们巩固前面掌握的知识,也可以更好地理解 Promise 各个方法的使用原理,以便在实践中更好地运用 Promise。

功能分析

在上一篇中学习了 Promise 的 3 个对象方法和 6 个类方法,我们要实现一个自己的 Promise (下面我暂时帮它命名为 doPromise),也需要实现这些方法。

通常给一个类命名,单词首字母需要大写;我是觉得这样看起来更顺眼,大家不要学我

Snap (3) (1).png

核心功能手写实现

constructor 构造器

Promise 处理回调的参数是必不可少的,这里先定义两个用于处理成功和失败的回调函数

class doPromise {
  constructor(executor) {    
    const resolve = (value) => {}
    const reject = (reason) => {}
  }
}

特性一:立即执行

代码实现非常简单,我们知道 constructor 是 class 中的一个特殊方法,在使用 new 关键字创建或初始化某个对象时,JavaScript 会立即调用对象的 constructor 方法

所以我们只需要在 constructor 配置回调函数,它会在我们创建 Promise 后立即执行

class doPromise {
  constructor(executor) {
    console.log('我现在就要执行!!');  // 给大家解释一下可以执行,后续就删除掉了
    
    const resolve = (value) => {}
    
    const reject = (reason) => {}
    
    executor(resolve, reject)
  }
}

new doPromise(() => {})  // 我现在就要执行!!

特性二:三种状态的变更

我们知道在 Promise 中有三种状态:pendingfulfilledrejected,分别表示 Promise 的初始状态,没有被敲定也没有被拒绝;已敲定状态,表示 Promise 执行成功;已拒绝状态,表示 Promise 执行失败。

为实现三种状态的变更,我们可以定义 三个变量用来控制这些状态

image.png

const PROMISE_STATUS_PENDING = 'pending'
const PROMISE_STATUS_FULFILLED = 'fulfilled'
const PROMISE_STATUS_REJECTED = 'rejected'

class doPromise {
  constructor(executor) {
    this.status = PROMISE_STATUS_PENDING  // 初始状态为 pending

    const resolve = (value) => {
      this.status = PROMISE_STATUS_FULFILLED  // 执行成功后,状态变更为 fulfilled
    }
    const reject = (reason) => {
      this.status = PROMISE_STATUS_REJECTED  // 执行失败后,状态变更为 rejected
    }
    executor(resolve, reject)
  }
}

特性三:状态不可逆

Promise 的状态一旦由 pending 转变为 fulfilled 或者 rejected,则状态锁定,后续任何调用都不会变更此状态。

所以,我们只需要在调用 resolve() 或 reject() 变更状态前,判断此时是否为 pending 状态即可:

const resolve = (value) => {
  if (this.status === PROMISE_STATUS_PENDING) {
    this.status = PROMISE_STATUS_FULFILLED
  }
}   
const reject = (reason) => {
  if (this.status === PROMISE_STATUS_PENDING) {
    this.status = PROMISE_STATUS_REJECTED
  }
}

3 个对象方法的实现

1. then() 方法

then() 方法是 Promise 中最复杂的一个方法,需要考虑到的因素是最多的

我们先来回顾一下 Promise 中的 then() 中通常是怎样的代码结构 👇

image.png

then() 中接收两个可选参数 onFulfilledonRejected 回调函数。

还需要定义两个值,valuereason 分别用于接收 执行成功的结果 和 执行失败的原因

在执行成功时,调用 resolve ,并把接收的值赋给 value;执行被拒绝时,调用 reject,拒绝原因赋值给 reason

其实在 resolve 和 reject 中赋值的代码不应该在这一部分写,而是在构造器中就已经应该存在的逻辑。但是,如果一个 promise 没有调用 then 方法,只是简单的定义,那么给 value 赋值也没有实际意义

总之,下面我们需要实现两部分功能:

  1. 在 resolve 和 reject 中 获取到 Promise 的结果并保存

  2. then()第一个回调函数 (onFulfilled) 中返回 value 的值(仅当状态为 fulfilled),第二个回调函数(onRejected)中返回 reason 原因(仅当状态为 rejected)

class doPromise {
  constructor(executor) {
    this.status = PROMISE_STATUS_PENDING
    this.value = undefined
    this.reason = undefined

    const resolve = (value) => {
      this.status = PROMISE_STATUS_FULFILLED
      this.value = value
      this.onFulfilled(this.value)
    }

    const reject = (reason) => {
      this.status = PROMISE_STATUS_REJECTED
      this.reason = reason
      this.onRejected(this.reason)
    }
    
    executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {
    this.onFulfilled = onFulfilled
    this.onRejected = onRejected
  }
}

TypeError: this.onFulfilled is not a function

是的,当你这个时候使用 promise.then(...) 方法去尝试执行的时候会报上面错误

原因:代码会首先执行 constructor 中的内容,此时 then() 方法还未执行,所以 this.onFulfilled 是 undefined,并不是一个函数

image.png

解决方法:

我们知道,setTimeout 可以把自身的内容作为异步执行,先放入 JavaScript 的任务队列里,在同步任务执行完成后,才会继续从任务队列中取出继续执行。

如果我们用 setTimeout 把 onFullfilled() 调用包裹起来,那么 onFullfilled() 就可以在同步任务即 then() 之后再进行调用,也就能获取到 then() 方法传递的两个回调函数赋值给 onFullfilled() 和 onRejected()

image.png

微任务 queueMicrotask

🧊 值得注意的是,根据我们的使用习惯也是为了方便理解,我刚刚使用了 setTimeout 作为异步说明的论证,但 Promise 调用后本身是一个微任务,setTimeout 属于宏任务,所以我们后续使用 queueMicrotask() 替换 setTimeout()queueMicrotask 在此处不作讲解。

多次调用

链式调用 我们知道,那什么是 多次调用 呢 ?

就比如下面这样:

const p1 = new doPromise((resolve, reject) => {
  resolve(200)
})

p1.then(res => {
  console.log(res);
}, err => {...})

p1.then(res => {
  console.log(res);
}, err => {...})

// 1

理论上来说,或者你尝试一下 Promise 就知道,正常情况下应该会打印 两次 resolve 结果 200,然而我们这里只会返回一次,思考下为什么 ?

还记得我们刚刚用了异步代码 queueMicrotask 吗,我们来看一下代码的具体执行情况:

image.png

  1. 创建 doPromise 实例并回调 resolve(200),此时状态由 pending 转变为 fulfilled,value 被赋值 200,queueMicrotask 添加到微任务队列,向后执行
  2. p1 第一次调用 then() 方法,打印 console.log 内容,并且 this.onFulfilled 被赋值,由于后续仍有同步任务,继续向后执行
  3. p1 第二次调用 then() 方法,打印 console.log 内容,但上一次 this.onFulfilled 的值被覆盖
  4. 执行异步代码,调用 this.onFulfilled

这就是我们的 Promise 为什么只会打印一次结果的原因,要实现多次调用应该如何调整代码呢?

这还不简单,既然是因为覆盖造成的,那我们不让它覆盖不就完了,有几次调用就创建几个异步任务。用一个 数组 分别接收执行成功和失败的回调,并在 回调中遍历执行 每个异步任务。

class doPromise {
  constructor(executor) {
    // ... 之前的省略,仅更新本次添加的内容
    this.onFulfilledFns = []
    this.onRejectedFns = []

    const resolve = (value) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        this.status = PROMISE_STATUS_FULFILLED
        this.value = value
        queueMicrotask(() => {
          this.onFulfilledFns.forEach(fn => {
            fn(this.value)  // 遍历每个回调
          })
        });
      }
    }
    
    // ... reject 同理,此处省略
    executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {
    this.onFulfilledFns.push(onFulfilled)  // 将每个方法保存到数组中
    this.onRejectedFns.push(onRejected)
  }
}

这次看起来逻辑就通顺了,它不是同步任务吗,管他几次调用,我们就在 then() 方法中把调用 push 到提前准备好的数组,等同步任务完成后,一个个遍历出来回调的内容再传值就 ok 了。

🔨 特殊情况:多次且异步调用

在调用成功后,再异步调用,是否还能输出正确的结果 ?

const p1 = new Promise((resolve, reject) => {
  resolve(200)   
})

p1.then(res => {
  console.log(res);
}, err => {...})

setTimeout(() => {
  p1.then(res => {
    console.log(res);
  }, err => {...})
}, 1000)  // 延迟 1s 后再次调用 p1
// 200

我们发现本应该输出两次的结果,此时又只出现了一次。这是因为,在 setTimeout() 中的 p1 执行 then() 方法时,p1 的状态已经变成 fulfilled,即便把回调添加到了数组中,也没办法再次执行了。

实际上在第一次调用 then() 之后,我们已经得到了对应的状态以及 value/reason 的值。该有的都有了,为什么不直接执行回调呢。所以我们可以这样解决:

then(onFulfilled, onRejected) {
  // 如果此时状态已经变成 fulfilled/rejected,说明已经有 value/reason,请大胆地执行该回调
  if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
    onFulfilled(this.value)
  }
  if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
    onRejected(this.reason)
  }
 
  // 仅当状态是 pending 时,向数组中 push 回调
  if (this.status === PROMISE_STATUS_PENDING) {
    this.onFulfilledFns.push(onFulfilled)
    this.onRejectedFns.push(onRejected)
  }
}

🔨🔨 你以为这就完事啦?NO!

刚刚新增的两条代码:在 then() 中进行状态的判断,当状态不是 pending 时,马上执行回调

是不是觉得有点不对劲,但又说不上来的感觉。明明已经做好了功能,逻辑说得通,测试没问题呀。执行回调的结果都可以正确拿到,调用次数也对得上。

我们再来看一下刚刚加完代码的逻辑:

image.png

  1. 我们在调用 resolve 之后,状态立即变更为 fulfilled,value 也被同步赋值;
  2. 此时调用 then() 对象方法,根据状态判断进入第一个作用域,直接执行传入的回调函数

得到的结果可能是正确的,但是我们会发现在 then() 方法中永远无法进入到 pending 状态的作用域中,那之前用来异步执行的 queueMicrotask 函数岂不是失去了意义。这样实质上会出现的问题是:

const p1 = new Promise((resolve, reject) => {
  resolve(200)
})
p1.then(res => {
  console.log(res)
}, err => {...})

console.log(10086)
// 200
// 10086

现在这个 Promise 不是异步的了! 那还是正经 Promise 吗?!

我们需要把状态判断和赋值的操作也转移到异步任务中,这样才不会在 then() 中被提前处理,最终处理 then() 方法的多次调用逻辑需要这样调整:

class doPromise {
  constructor(executor) {
    // ... 之前的省略,仅更新本次添加的内容

    const resolve = (value) => {
      if (this.status === PROMISE_STATUS_PENDING) {
        // 状态判断、赋值全部移到异步任务中
        queueMicrotask(() => {
          if (this.status !== PROMISE_STATUS_PENDING) return
          this.status = PROMISE_STATUS_FULFILLED
          this.value = value
          this.onFulfilledFns.forEach(fn => {
            fn(this.value)  // 遍历每个回调
          })
        });
      }
    }
    
    // ... rejected 同理,此处省略
  }
}

链式调用

解决完多次调用的问题,就要来思考链式调用的实现方法了。毕竟 Promise 的出现有一大部分原因是为了 处理回调地狱,而 链式调用 就是解决这个问题的一大方法。

那么我们首先思考通常怎样使用链式调用,它的原理又是什么?

使用链式调用,通常是由于获取最终数据的参数依赖于另一个请求,总之是一步无法完成完整的业务内容。

image.png

所以,Promise 链式调用的本质then() 方法的返回值,仍是一个 Promise 实例。只有这样,后续才能接着调用 then() 方法和 catch() 方法

image.png

把 then() 方法中的内容用我们自己的 Pormise 包裹起来,再把 return 出的返回值 resolve 出去。

image.png

then(onFulfilled, onRejected) {
  return new doPromise((resolve, reject) => {
    if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
      const value = onFulfilled(this.value)
      resolve(value)
    }
    if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
      const reason = onRejected(this.reason)
      resolve(reason)
    }
 
    if (this.status === PROMISE_STATUS_PENDING) {
      this.onFulfilledFns.push(() => {
        const value = onFulfilled(this.value)
        resolve(value)
      })
      this.onRejectedFns.push(() => {
        const reason = onRejected(this.reason)
        resolve(reason)
      })
    }
  })
}

错误处理

错误处理分很多种,包括自身抛出的异常和代码运行的异常

当在 Promise 中发生异常错误时,我们可以使用 try/catch 捕获并在 constructor 中执行 reject()

class doPromise {
  constructor(executor) {
    // ... 省略 resolve、rejected 等代码

    try {
      executor(resolve, reject)
    } catch (err) {
      reject(err)
    }
  }
}

由于刚刚在实现链式调用时,我们把 then() 的返回值也包装成了一个 Promise,所以在 then() 中也应该做对应的错误处理

then(onFulfilled, onRejected) {
  return new doPromise((resolve, reject) => {
    if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
      try {
        const value = onFulfilled(this.value)
        resolve(value)
      } catch (err) {
        reject(err)
      }
    }
    // ... 后面也是一样的处理方式
  })
}

后面的代码也是一样的处理方式,我们可以定义一个错误处理方法来优化代码:

function execFunctionWithCatchError(execFn, value, resolve, reject) {
  try {
    const result = execFn(value)
    resolve(result)
  } catch(err) {
    reject(err)
  }
}

将定义好的方法使用在 then() 中就可以这么写

then(onFulfilled, onRejected) {
  return new doPromise((resolve, reject) => {
    if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
      execFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
    }
    if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
      execFunctionWithCatchError(onRejected, this.reason, resolve, reject)
    }
    
    if (this.status === PROMISE_STATUS_PENDING) {
      this.onFulfilledFns.push(() => {
        execFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
      })
      this.onRejectedFns.push(() => {
        execFunctionWithCatchError(onRejected, this.reason, resolve, reject)
      })
    }
  })
}

2. catch() 方法

通过上一节的内容其实我们知道,catch() 方法的根本就是 then() 方法第二个回调函数的语法糖

它可以让我们在 then() 方法中专心关注 fulfilled(敲定状态)返回的值,在 catch() 方法中处理 Promise 执行过程中出现的异步错误。

catch(onRejected) {
  return this.then(undefined, onRejected)
}

不过我们还需要在 then() 方法中处理回调函数为 undefined 的情况

then(onFulfilled, onRejected) {
  // 定义两个默认的回调函数,用于处理在未传回调时的默认处理方式
  // 1. 如果 onRejected 未传值,则抛出异常给下一个 Promise 处理
  const defaultOnRejected = err => { throw err }
  onRejected = onRejected || defaultOnRejected

  // 2. 如果 onFulfilled 未传值,则返回结果给下一个 Promise 处理
  const defaultOnFulfilled = value => { return value }
  onFulfilled = onFulfilled || defaultOnFulfilled

  return new doPromise((resolve, reject) => {
    // ...
    if (this.status === PROMISE_STATUS_PENDING) {
      // 添加可选参数判断,防止在没有回调时报错
      if (onFulfilled) this.onFulfilledFns.push(() => {
        execFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
      })
      if (onRejected) this.onRejectedFns.push(() => {
        execFunctionWithCatchError(onRejected, this.reason, resolve, reject)
      })
    }
  })
}

3. finally()

通过上面 catch() 方法的实现,再来思考 finally() 的话就会清晰很多

finally() 主要的作用就是在 Pormise 执行完成后调用的函数,它无须接收任何参数

finally(onFinally) {
  // 无论状态是敲定还是拒绝,都执行传入的 onFinally 方法
  this.then(() => {
    onFinally()
  }, () => {
    onFinally()
  })
}

6 个类方法的实现

类方法需要加上 static 前缀

1. Promise.resolve()

static resolve(value) {
  return new doPromise(resolve => resolve(value))
}

2. Promise.reject()

static reject(reason) {
  return new doPromise((resolve, reject) => reject(reason))
}

3. Promise.all()

all() 方法的使用是在 Promise 中出现频率较高的一个,也是面试中常考的问题

而且在网站的性能优化中,也常用来并行加载多个资源,优化页面加载速度和用户体验

all() 方法使用原理分析:

  1. all() 接收一组 Promise
  2. Promises 全部执行成功,在最后一个 Promise 状态敲定时,返回这组 Promises 的所有执行结果(返回的执行结果顺序由数组内 Promise 顺序决定,不受 resolve 的先后影响)
  3. Promises 中任意一个执行失败,立即返回拒绝原因

image.png

因为 Promise.all() 的结果也要在 then() 方法中接收和处理结果,所以 all() 方法返回一个 Promise

定义一个数组 results 用于存储 Promises 成功的结果,遍历数组中每个 Promise 的执行结果

全部执行完成且成功时,按照数组的传入顺序返回对应结果,如果发生错误立即返回 reject 错误原因

将数组遍历执行其实很好理解,这里需要注意的是两点:

  1. 执行成功时,按照数组的传入顺序返回对应结果
  2. 如果传入的不是 promise,由于没有 then 方法调用时会报错,所以直接返回原值
static all(promises) {
  return new doPromise((resolve, reject) => {
    const results = []  // 接收 Promises 成功的结果
    let fulfilledTimes = 0  // 执行成功的次数
    
    const pushResult = (_index, _value) => {
      results[_index] = _value
      fulfilledTimes++
      // 若执行成功的次数和Promises数组长度相等时,返回结果
      if (fulfilledTimes === promises.length) {
        resolve(results)
      }
    }
    
    promises.forEach((promise, index) => {
      if (promise instanceof doPromise) {
        promise.then(res => {
          pushResult(index, res)
        }, err => {
          reject(err)
        })
      } else {
        pushResult(index, promise)
      }
    })
  })
}

4. Promise.allSettled()

all()allSettled() 的区别在于,allSettled() 不会将错误暴露出来,而是和所有结果一起 resolve 出来

image.png

有了上面的经验,allSettled() 就容易理解了

在 Promise 被拒绝时,也将结果保存在数组中,等待全部 Promises 执行完成,一起返回其结果

static allSettled(promises) {
  return new doPromise(resolve => {
    const results = []
    let fulfilledNum = 0
    promises.forEach((promise, index) => {
      if (promise instanceof doPromise) {
        promise.then(res => {
          // 只是把 results 数组中的值换成了对象
          results[index] = { status: PROMISE_STATUS_FULFILLED, value: res }
          fulfilledNum++
          if (fulfilledNum === promises.length) {
            resolve(results)
          }
        }, err => {
          // promise 被拒绝时不影响后续执行,只将状态和原因进行保存
          results[index] = { status: PROMISE_STATUS_REJECTED, value: err }
          fulfilledNum++
          if (fulfilledNum === promises.length) {
            resolve(results)  
          }
        })
      } else {
        results[index] = { status: PROMISE_STATUS_FULFILLED, value: promise }
        fulfilledNum++
      }
    })
  })
}

5. Promise.race()

无论结果如何,只要第一个出来的结果

这里就不再需要定义数组接收了,因为 race() 只返回一个结果

image.png

这样就更容易了,我们只需要拿到最先出结果的 promise ,根据其状态调用不同的回调函数就可以

static race(promises) {
  return new doPromise((resolve, reject) => {
    promises.forEach(promise => {
      if (promise instanceof doPromise) {
        // 这些 promise 都是异步的,最快调用 then 方法的 promise 结果作为返回值
        promise.then(res => {
          resolve(res)
        }, err => {
          reject(err)
        })
      } else {
        // 普通值直接返回原值
        resolve(promise)
      }
    })
  })
}

6. Promise.any()

any()race() 的区别在于两点

  1. 只返回最先 成功 的那一条结果
  2. 如果这一组的所有 promise 都被拒绝,返回错误提示

image.png

当所有 promise 都被拒绝时,返回错误,这就说明了需要有一个数组去接收错误

第一个回调 resolve 和 race() 一样,只需返回最快成功的 promise

static any(promises) {
  const reasons = []  // 接收拒绝原因,在所有 promise 都被拒绝时返回
  return new doPromise((resolve, reject) => {
    promises.forEach(promise => {
      if (promise instanceof doPromise) {
        promise.then(res => {
          resolve(res)
        }, err => {
          reasons.push(err)
          if (reasons.length === promises.length) {
            reject(new AggregateError([reasons], 'All Promises rejected'))
          }
        })
      } else {
        resolve(promise)
      }
    })
  })
} 

结语

到这里,实现一个简单的 Promise 已经结束了;这虽然不是一个完整的 Promise,还有很多情况在上面没有考虑到(大家可以自己补充也可以发表在评论里帮助其他同学),不过也是对 Promise 系列前两篇文章知识点的一个巩固,希望大家能够更好的理解 Promise 对象。

也十分感谢小伙伴们能看到这里,如果文中有我理解错误或者不对的地方也希望大家能帮我指出错误,不要让我误人子弟 😳

后续在准备面试的时候,应该会再开一个面试题系列专栏,到时候会收集一些 Promise 相关的面试题,汇总一下。