如果你还没实现过Promise

425 阅读8分钟

前言

手写Promise,老生常谈的话题,但是实际使用时,难免还是会踩坑,今天就来一步步实现它,摸摸它到底是何方神圣。如果你还没有手写过 Promise,或者你想温习下 Promise 的实现,获取你可以看看这边文章。

接下来就是一步步实现一个符合 PromiseA+ 规范的Promise的过程。

喏,这里:PromiseA+

V0:基本实现

Promise状态pending(等待)、fulfilled(成功)、rejected(失败)

Promise的雏形

class MyPromise {
  constructor(executor) {
    this.status = 'pending' // 状态
    this.value = undefined // 成功的结果
    this.reason = undefined // 失败的原因

    let resolve = () => {}
    let reject = () => {}

    executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {}
}

executor 容错

    // 传入的执行函数可能会抛出错误
    try {
      // 将resolve和reject给使用者
      executor(resolve, reject)
    } catch (e) {
      // error注入reject
      reject(e)
    }

resolve 和 reject 方法

    // 定义resolve
    let resolve = data => {
      // 只能从pending变为fulfilled或者rejected状态
      if (this.status === 'pending') {
        this.status = 'fulfilled'
        this.value = data
      }
    }
    // 定义reject
    let reject = data => {
      // 只能从pending变为fulfilled或者rejected状态
      if (this.status === 'pending') {
        this.status = 'rejected'
        this.reason = data
      }
    }

then 方法

  // 定义then方法,将fulfilled或者rejected的结果传入onFulfilled或者onRejected中
  then(onFulfilled, onRejected) {
    if (this.status === 'fulfilled') {
      onFulfilled(this.value)
    }
    if (this.status === 'rejected') {
      onRejected(this.reason)
    }
  }

那么,一个简单的实现就完成啦。

小测试:

let p = new MyPromise((resolve, reject) => resolve(1))
p.then(res => console.log(res)) // 1

上面的例子可以正常输出结果,但是,如果Promise内部是异步的呢?

let q = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(2)
  }, 0)
})
q.then(res => console.log(res)) // 并没有输出

由于异步延迟,调用then方法时,状态还是 pending,无法调用onFulfilled或者onRejected,那么我们需要对异步的情况做相应的处理。

V1:加入异步处理

怎么处理异步情况?

针对上面的分析,调用then方法时还是 pending 状态,那么此时应该将回调函数存起来,等到状态改变(fulfilled / rejected)时再取出来调用。考虑到可能存在多个回调函数,我们使用数组存储回调函数,形成回调队列。

1. 定义两个数组作为回调队列

this.onResolvedCallbacks = [] // 存放成功的回调
this.onRejectedCallbacks = [] // 存放失败的回调

2. then 方法中处理 pending 状态下的回调函数

  then(onFulfilled, onRejected) {
    if (this.status === 'fulfilled') {
      onFulfilled(this.value)
    }
    if (this.status === 'rejected') {
      onRejected(this.reason)
    }
    // 新增:当 Promise还是等待状态,存储回调函数
    if (this.status === 'pending') {
      this.onResolvedCallbacks.push(onFulfilled)
      this.onRejectedCallbacks.push(onRejected)
    }
  }

3. 调用回调队列的函数

什么时候调用回调队列?

由于 resolve 或者 reject 时是在异步队列里,我们在 then 中已经存储了相应的回调函数,那么当状态改变时,即在 resolve 或者 reject 发生时,就可以将回调函数取出来依次调用。

修改下constructor 方法:

    let resolve = data => {
      if (this.status === 'pending') {
        this.status = 'fulfilled'
        this.value = data
        // 状态改变后取出回调队列的函数依次调用
        this.onResolvedCallbacks.forEach(cb => cb(this.value))
      }
    }
    let reject = data => {
      if (this.status === 'pending') {
        this.status = 'rejected'
        this.reason = data
        // 状态改变后取出回调队列的函数依次调用
        this.onRejectedCallbacks.forEach(cb => cb(this.reason))
      }
    }

我们在存储回调函数时,value 或者 reason还没有值,等到状态改变时才拿到值,因此依次调用回调队列的函数时将相应的 value 或者 reason 传入。

测试一下加入异步是否可以正常输出

let q = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(2)
  }, 0)
})
q.then(res => console.log(res)) // 2

异步处理,完成。

就这?但是,别忘了还有 Promise 的链式调用呢!

V2:实现链式调用

基于前面的实现是没法实现链式调用的。

q.then(res => {
  console.log(res)
  return 3
}).then(res => console.log(res)) // Uncaught TypeError: Cannot read property 'then' of undefined

别忘了Promises/A+规范要求:then 方法返回新的 Promise。

接下来完善 then 方法

规范化then 参数:onFulfilled 和 onRejected

    // onFulfilled 和 onRejected 为函数
    // onFulfilled 不是函数时包装成函数,返回传入的值
    onFulfilled =
      typeof onFulfilled === 'function' ? onFulfilled : value => value
    // onRejected 不是函数时,需要抛出错误,否则会在后面的链式调用被resolve捕获
    onRejected =
      typeof onRejected === 'function' ? onRejected : error => {throw error}    
      

then 方法返回新的 Promise,并加入 try...catch

  then(onFulfilled, onRejected) {
    // onFulfilled 和 onRejected 为函数
    // onFulfilled 不是函数时包装成函数,返回传入的值
    onFulfilled =
      typeof onFulfilled === 'function' ? onFulfilled : value => value
    // onRejected 不是函数时需要抛出错误,否则会在后面的链式调用被resolve捕获!!!
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : error => {
            throw error
          }

    const promise = new MyPromise((resolve, reject) => {
      if (this.status === 'fulfilled') {
        try {
          let x = onFulfilled(this.value)
          resolve(x)
        } catch (e) {
          reject(e)
        }
      }
      if (this.status === 'rejected') {
        try {
          let x = onRejected(this.reason)
          resolve(x)
        } catch (e) {
          reject(e)
        }
      }
      // 当 Promise还是等待状态,存储回调函数
      if (this.status === 'pending') {
        this.onResolvedCallbacks.push(() => {
          try {
            let x = onFulfilled(this.value)
            resolve(x)
          } catch (e) {
            reject(e)
          }
        })
        this.onRejectedCallbacks.push(() => {
          try {
            let x = onRejected(this.reason)
            resolve(x)
          } catch (e) {
            reject(e)
          }
        })
      }
    })
    return promise
 }

这下再执行链式调用就不报错了

q.then(res => {
  console.log(res)
  return 3
}).then(res => console.log(res)) // 一次输出 2 3

q.then()then().then(res => console.log(res)) // 2, 依次传递,因此输出2

到这里,基本就实现了支持链式调用的Promise了。

但是,但是,但是,是的,又双叒但是了,onFulfilled 和 onRejected 里面我们可以返回任何值,原始数据类型、引用类型、甚至是Promise!基于上面的实现,还不足以处理所有的返回值。不信你看:

q.then(res => {
  console.log(res)
  return new MyPromise((resolve) => resolve(3))
}).then(res => console.log(res)) // 依次输出 2 MyPromise

但是实际上,Promise 针对 onFulfilled 和 onRejected 里面返回 Promise 时,是会步步往里走,直到拿到一个里面的值的。也就是说,他实际上会输出 2 3。

这就迎来了下一版本。

我们将 then 中的 resolve 的逻辑抽取出来,用完善版的 resolvePromise 代替。

V3:引入 resolvePromise 方法

我们希望这个函数处理什么呢?

  • 处理 onFulfilled 和 onRejected 的返回值 x,onFulfilled 时需要 resolve,onRejected 时需要 reject
  • 循环引用:当 then 的返回值 promise 与 x 是同一引用时,抛出TypeError错误(2.3.1)

resolvePromise需要的参数

基于上面的分析,这个函数长这样

/**
 * 处理then里面onFulfilled或onRejected的返回值
 * @param {Object} promise then方法返回的Promise对象
 * @param {*} x onFulfilled或onRejected的返回值
 * @param {Function} resolve Promise构造函数的resolve方法
 * @param {Function} reject Promise构造函数的reject方法
 */
function resolvePromise(promise, x, resolve, reject) {
  // 
}

循环引用时抛出 TypeError 错误

if (promise === x) {
    return reject(new TypeError('Promise循环引用啦'))
}

处理返回值

大概如此:

  // 返回值x 为 Promise(2.3.2)
  // 这一节其实可以省略,因为在下面对then的处理中已经包含了
  // if (x instanceof Promise) {}

  // 返回值x是对象或者函数(2.3.3)包含x为Promise的情况(2.3.2)
  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    // try...catch 防止then出现异常
    try {
      let then = x.then // (2.3.3.1)
      /
    } catch (e) {
      reject(e) // (2.3.3.2)(2.3.3.3.4.2)
    }
  } else {
    // 返回值x只是一个普通值(2.3.4)
    resolve(x)
  }

接下来,处理下返回值的复杂场景

  // 返回值x 为 Promise(2.3.2)
  // 这一节其实可以省略,因为在下面对then的处理中已经包含了
  // if (x instanceof Promise) {}

  let called = false // 是否 resolve 或者 reject了
  // 返回值x是对象或者函数(2.3.3)包含x为Promise的情况(2.3.2)
  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    // try...catch 防止then出现异常
    try {
      let then = x.then // (2.3.3.1)
      if (typeof then === 'function') {
        //  返回值x有then且then是一个函数,则调用它,并将this指向x(2.3.3.3)
        then.call(
          x,
          y => {
            if (called) return // 已经 resolve 或者 reject了, 则忽略(2.3.3.3.3)
            called = true
            // y有可能还是一个Promise,递归处理
            resolvePromise(promise, y, resolve, reject)
          },
          r => {
            if (called) return // 已经 resolve 或者 reject了, 则忽略(2.3.3.3.3)
            called = true
            reject(r)
          }
        )
      } else {
        // 只是一个普通对象或者普通函数,则直接resolve
        resolve(x)
      }
    } catch (e) {
      if (called) return // 已经 resolve 或者 reject了, 则忽略(2.3.3.3.4.1)
      called = true
      reject(e) // (2.3.3.2)(2.3.3.3.4.2)
    }
  } else {
    // 返回值x只是一个普通值(2.3.4)
    resolve(x)
  }

V4: 给then中所有的返回加上异步延迟(标准版)

这里使用setTimeout模拟延迟

  then(onFulfilled, onRejected) {
    // ...
    
    let promise
    promise = new MyPromise((resolve, reject) => {
      if (this.status === 'fulfilled') {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value)
            resolvePromise(promise, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }
      if (this.status === 'rejected') {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            resolvePromise(promise, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }
      // 当 Promise还是等待状态,存储回调函数
      if (this.status === 'pending') {
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value)
              resolvePromise(promise, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason)
              resolvePromise(promise, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
      }
    })
    return promise
  }

标准版实现:MyPromise

测试是否符合PromiseA+规范

使用 promises-aplus-tests 这个库,测试你的Promise是否符合PromiseA+

yarn add promises-aplus-tests

测试前,需要在你的Promise结尾加上:

MyPromise.defer = MyPromise.deferred = function () {
  let defer = {}
  defer.promise = new MyPromise((resolve, reject) => {
    defer.resolve = resolve
    defer.reject = reject
  })
  return defer
}
try {
  module.exports = MyPromise
} catch (e) {}

下面就可以测试了

npx promises-aplus-tests Promise.js

基于上面的实现,可以看到一连串绿勾勾,最后看到 872 passing (17s),通过了测试。

V5: 加上周边方法(现代完整版)

其实基于V4已经够用了,也通过了测试。但是使用Promise时,我们是可以直接使用Promise.resolvePromise、reject等方法的,V5即在V4的基础上,补充了resolve、reject、catch、finally、all、race、allSettled等的实现。这里称之为 “现代完整版”。

现代完整版实现:Promise_pro

Q & A

1. 为何又谈论这个被谈烂的话题?

时常回顾和温习,是非天资聪颖者最后的倔强。若同样有助于你,万分荣幸。

2. 为什么给then中所有的返回加上延迟要使用setTimeout?

首先,Promise 本身是同步的,其 then 和 catch 方法是异步的, 这里使用 setTimeout 模拟异步,是符合Promise A+规范的,也通过了测试,当然你也可以使用其它方法,如 MutationObserver

虽然在 Eventloop 中setTimeout 属于宏任务,而实际上,Promise 的 then和 catch 是属于微任务队列,该实现与实际是有些许差异的,但这并影响我们通过手写一个符合 Promise A+ 规范的 Promise 去理解它的原理。

参考

感谢大佬铺路,助我前行