手撕 Promise


字节前端实习一面就让我手写 Promise,没写出直接挂了 💔,亡羊补牢,为时不晚。

使用方法回顾

作为 ES6 提出来的新语法,首先回顾一下 Promise 的使用方法。

const promise = new Promise((reslove, reject) => {
  reslove(233)
})

promise.then((value) => {
  console.log(value) // 233
})

简单来说就是实例化一个 Promise 对象,实例化的时候传一个函数进去,这个函数有两个变量:

  • resolve,是一个回调函数,用于处理成功的数据
  • reject,也是一个回调函数,用于处理失败的信息

简易版 Promise

简易版 Promise 🍣 呈上:

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

class MyPromise {
  constructor(executor) {
    // 实例化的时候立刻执行传入的这个参数
    executor(this.resolve, this.reject)
  }

  // 初始化状态为 pending
  status = PENDING

  // value 存储成功的结果,reason 存储失败的结果
  value = null
  reason = null

  resolve = (value) => {
    // 只有当状态为 pending 的时候才执行成功回调
    if (this.status === PENDING) {
      this.status = FULLFILLED
      this.value = value
    }
  }

  reject = (reason) => {
    // 只有当状态为 pending 的时候才执行失败回调
    if (this.status === PENDING) {
      this.status = REJECTED
      this.reason = reason
    }
  }

  then(onFullfilled, onRejected) {
    if (this.status === FULLFILLED) {
      // 如果传入的实例的状态是成功,则执行成功回调
      onFullfilled(this.value)
    } else if (this.status === REJECTED) {
      // 如果传入的实例的状态是失败,则执行失败回调
      onRejected(this.reason)
    }
  }
}

测试成功:

const promise = new MyPromise((reslove, rejectct) => {
  reslove(233)
})

promise.then((value) => {
  console.log(value) // 233
})

然而这样子的代码是无法实现异步操作的,因为我们的 then 方法是在主线程中直接运行的,不会受到 executor(也就是新建 Promise 对象的时候传的函数)里异步操作的限制。

异步操作扩展

因此,为了允许异步操作,我们要进行以下扩展:

// 存储成功回调函数
onFulfilledCallback = null
// 存储失败回调函数
onRejectedCallback = null

resolve = (value) => {
  // 只有当状态为 pending 的时候才执行成功回调
  if (this.status === PENDING) {
    this.status = FULFILLED
    this.value = value
    // 如果存在成功回调,就调用
    this.onFulfilledCallback && this.onFulfilledCallback(value)
  }
}

reject = (reason) => {
  // 只有当状态为 pending 的时候才执行失败回调
  if (this.status === PENDING) {
    this.status = REJECTED
    this.reason = reason
    // 如果存在成失败回调,就调用
    this.onRejectedCallback && this.onRejectedCallback(reason)
  }
}

then(onFulfilled, onRejected) {
  if (this.status === FULFILLED) {
    onFulfilled(this.value)
  } else if (this.status === REJECTED) {
    onRejected(this.reason)
  } else if (this.status === PENDING) {
    // 如果传入的实例仍是等待中,那么需要先存储成功和失败的回调函数
    this.onFulfilledCallback = onFulfilled
    this.onRejectedCallback = onRejected
  }
}

但是这个方式有个很明显的缺陷,就是我们的 promise 一旦有多个 then,它的原先的回调函数就会被新的回调函数覆盖掉。

const promise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 1000)
})

promise.then((value) => {
  console.log(1)
  console.log(value)
})

promise.then((value) => {
  console.log(2)
  console.log(value)
})

promise.then((value) => {
  console.log(3)
  console.log(value)
})

// 3
// success

很容易就能想到,用数组来存储所有的成功回调和失败回调函数即可。

// 存储成功回调函数
onFulfilledCallback = []
// 存储失败回调函数
onRejectedCallback = []

resolve = (value) => {
  // 只有当状态为 pending 的时候才执行成功回调
  if (this.status === PENDING) {
    this.status = FULFILLED
    this.value = value
    // 如果存在成功回调,就调用
    while (this.onFulfilledCallback.length) {
      // 这里用 shift() 保证了每个成功回调只会运行一次
      this.onFulfilledCallback.shift()(value)
    }
  }
}

reject = (reason) => {
  // 只有当状态为 pending 的时候才执行失败回调
  if (this.status === PENDING) {
    this.status = REJECTED
    this.reason = reason
    // 如果存在成失败回调,就调用
    while (this.onRejectedCallback.length) {
      this.onRejectedCallback.shift()(value)
    }
  }
}

then(onFulfilled, onRejected) {
  if (this.status === FULFILLED) {
    // 如果传入的实例的状态是成功,则执行成功回调
    onFulfilled(this.value)
  } else if (this.status === REJECTED) {
    // 如果传入的实例的状态是失败,则执行失败回调
    onRejected(this.reason)
  } else if (this.status === PENDING) {
    // 如果传入的实例仍是等待中,那么需要先存储成功和失败的回调函数
    this.onFulfilledCallback.push(onFulfilled)
    this.onRejectedCallback.pop(onRejected)
  }
}

链式调用

我们一鼓作气,再把 then 的链式调用给实现了。

逻辑也很清晰,就是在调用 then 的时候,我们需要判断一下,执行成功回调函数的返回值是否是一个 MyPromise 对象,两种情况:

  • MyPromise 对象,就继续调用返回的这个对象的 then 方法
  • 是普通值,就直接调用成功的回调函数
then(onFulfilled, onRejected) {
  // 为了实现链式调用,需要返回一个 MyPromise 对象
  return new MyPromise((resolve, reject) => {
    if (this.status === FULFILLED) {
      // 获取成功回调的结果
      const ret = onFulfilled(this.value)
      if (ret instanceof MyPromise) {
        // 如果返回值仍然是一个 MyPromise 实例,则进行链式调用
        ret.then(resolve, reject)
      } else {
        // ret 是普通值,则直接调用成功回调
        resolve(ret)
      }
    } else if (this.status === REJECTED) {
      // 如果传入的实例的状态是失败,则执行失败回调
      onRejected(this.reason)
    } else if (this.status === PENDING) {
      // 如果传入的实例仍是等待中,那么需要先存储成功和失败的回调函数
      this.onFulfilledCallback.push(onFulfilled)
      this.onRejectedCallback.pop(onRejected)
    }
  })
}

测试结果:

promise
  .then((value) => {
    console.log(1)
    console.log(value)
    return new MyPromise((resolve) => resolve('success'))
  })
  .then((value) => {
    console.log(2)
    console.log(value)
    return new MyPromise((resolve) => resolve('success'))
  })
  .then((value) => {
    console.log(3)
    console.log(value)
  })
// 1
// success
// 2
// success
// 3
// success

但是,这个实现还有很多限制,我们逐步解决。

循环调用

按我们上面的写法,我们如果在 promise.then 里面返回了同一个 promise,结果会报 Uncaught ReferenceError 的错误。

const promise = new MyPromise((resolve, reject) => {
  resolve('success')
})

const p = promise.then((value) => {
  console.log(value)
  return p
})

image-20210423113756496

但是,官方的 Promise 报的却是 Type Error 的错误。

const promise = new Promise((resolve, reject) => {
  resolve('success')
})

const p = promise.then((value) => {
  console.log(value)
  return p
})

image-20210423113849223

该怎么还原呢,我们尝试修改 then 方法。

then(onFulfilled, onRejected) {
  // 为了实现链式调用,需要返回一个 MyPromise 对象
  const p = new MyPromise((resolve, reject) => {
    if (this.status === FULFILLED) {
      // 创建一个微任务
      queueMicrotask(() => {
        // 获取成功回调的结果
        try {
          const ret = onFulfilled(this.value)
          if (p === ret) {
            // 如果返回值和原 promise 相同,抛出循环调用异常
            return reject(
              new TypeError('Chaining cycle detected for promise #<Promise>')
            )
          }
          if (ret instanceof MyPromise) {
            // 如果返回值仍然是一个 MyPromise 实例,则进行链式调用
            ret.then(resolve, reject)
          } else {
            // ret 是普通值,则直接调用成功回调
            resolve(ret)
          }
        } catch (error) {
          reject(error)
        }
      })
    } else if (this.status === REJECTED) {
      // 如果传入的实例的状态是失败,则执行失败回调
      onRejected(this.reason)
    } else if (this.status === PENDING) {
      // 如果传入的实例仍是等待中,那么需要先存储成功和失败的回调函数
      this.onFulfilledCallback.push(onFulfilled)
      this.onRejectedCallback.pop(onRejected)
    }
  })

  return p
}

在这里,还记得之前为了实现链式调用而新建了 MyPromise 对象,当时是直接 return 了这个对象,现在我们对其命名为 p,并且保存下来,以便后续判断循环调用。

只需要在获取成功回调返回值的时候,判断其是否和我们新建的 p 相同

由于 pNew 完之后才能获得的一个对象,所以直接在原本的 then 函数中增加判重逻辑即可。

验证:

const promise = new MyPromise((resolve, reject) => {
  resolve('new success')
})

// 定义一个循环调用的 promise
const p = promise.then((value) => {
  console.log(1)
  console.log(value)
  return p
})

p.then(
  (value) => {
    console.log(2)
    console.log(value)
  },
  (reason) => {
    console.log(3)
    console.log(reason)
  }
)

image-20210423192328052

捕获错误

在 Promise 中,为了避免发生错误就中断代码运行,我们需要对两个地方的错误进行捕获。

  1. 在实例化 MyPromise 的时候

  2. then 的过程中

针对实例化的错误肯定是在构造函数中进行捕捉,实际上只需要对 executor 执行的增加异常捕获操作。

constructor(executor) {
  try {
    // 实例化的时候立刻执行传入的这个参数
    executor(this.resolve, this.reject)
  } catch (error) {
    // 捕获错误并执行失败回调
    this.reject(error)
  }
}

针对 then 方法中出现的错误,显然就是处理对其进行异常捕获啦。

...
queueMicrotask(() => {
  // 获取成功回调的结果
  try {
    const ret = onFulfilled(this.value)
    if (p === ret) {
      // 如果返回值和原 promise 相同,抛出循环调用异常
      return reject(
        new TypeError('Chaining cycle detected for promise #<Promise>')
      )
    }
    if (ret instanceof MyPromise) {
      // 如果返回值仍然是一个 MyPromise 实例,则进行链式调用
      ret.then(resolve, reject)
    } else {
      // ret 是普通值,则直接调用成功回调
      resolve(ret)
    }
  } catch (error) {
    reject(error)
  }
})
...

让我们来看看效果是否和我们预期一样。

构造函数中捕获错误:

const promise = new MyPromise((resolve, reject) => {
  throw new Error('new failed')
  resolve('success')
})

image-20210423191321826

then 中捕获错误:

const promise = new MyPromise((resolve, reject) => {
  resolve('new success')
})

promise
  .then(
    (value) => {
      console.log(1, value)
      throw new Error('then failed')
    },
    (reason) => {
      console.log(2, reason)
    }
  )
  .then(
    (value) => {
      console.log(3, value)
    },
    (reason) => {
      console.log(4, reason)
    }
  )

image-20210423192718969

优化 then 中的 REJECTED 和 PENDING 状态

先前我们对 then 方法中 FULFLLED 状态的操作进行了优化,包括:

  • 创建微任务,实现检测循环调用错误
  • 执行回调时增加捕获异常功能

现在我们也对 REJECTEDPENDING 状态进行以上补充。

首先为了书写方便,先定义一个处理 promise 返回值的函数:

function resolvePromise(p, ret, resolve, reject) {
  if (p === ret) {
    // 如果返回值和原 promise 相同,抛出循环调用异常
    return reject(
      new TypeError('Chaining cycle detected for promise #<Promise>')
    )
  }
  if (ret instanceof MyPromise) {
    // 如果返回值仍然是一个 MyPromise 实例,则进行链式调用
    ret.then(resolve, reject)
  } else {
    // ret 是普通值,则直接调用成功回调
    resolve(ret)
  }
}

修改 then 方法:

then(onFulfilled, onRejected) {
  // 为了实现链式调用,需要返回一个 MyPromise 对象
  const p = new MyPromise((resolve, reject) => {
    if (this.status === FULFILLED) {
      // 创建一个微任务
      queueMicrotask(() => {
        // 获取成功回调的结果
        try {
          const ret = onFulfilled(this.value)
          resolvePromise(p, ret, resolve, reject)
        } catch (error) {
          reject(error)
        }
      })
    } else if (this.status === REJECTED) {
      // 如果传入的实例的状态是失败,则执行失败回调
      queueMicrotask(() => {
        try {
          const ret = onRejected(this.reason)
          resolvePromise(p, ret, resolve, reject)
        } catch (error) {
          reject(error)
        }
      })
    } else if (this.status === PENDING) {
      // 如果传入的实例仍是等待中,那么需要先存储成功和失败的回调函数
      this.onFulfilledCallback.push(() => {
        queueMicrotask(() => {
          try {
            const ret = onFulfilled(this.value)
            resolvePromise(p, ret, resolve, reject)
          } catch (error) {
            reject(error)
          }
        })
      })
      this.onRejectedCallback.push(() => {
        queueMicrotask(() => {
          try {
            const ret = onRejected(this.value)
            resolvePromise(p, ret, resolve, reject)
          } catch (error) {
            reject(error)
          }
        })
      })
    }
  })

  return p
}

then 方法中的默认函数

在调用 then 方法的时候,应该允许不写成功和失败的回调。

then(onFulfilled, onRejected) {
  // 未指定成功和失败回调时,使用默认函数
  onFulfilled =
    typeof onFulfilled === 'function' ? onFulfilled : (value) => value
  onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : (reason) => {
          throw reason
        }
  ...
}

resolve 和 reject 的静态调用

原生的 Promise 是支持无需实例化,直接调用 Promise.resolve() 来返回一个 Promise 对象的。

如果我们尝试用自定义的 MyPromise 进行这种操作:

MyPromise.resolve()
  .then(() => {
    console.log('1')
    return MyPromise.resolve('success')
  })
  .then((value) => {
    console.log(2, value)
  })

报错提示如下:

image-20210423200556984

因为是需要调用类方法,所以我们需要用 static 关键字声明 resolvereject 的静态方法。

class MyPromise {
  ...
  // resolve 静态方法
  static resolve(parameter) {
    if (parameter instanceof MyPromise) {
      return parameter
    }
    return new MyPromise((resolve) => {
      resolve(parameter)
    })
  }

  // reject 静态方法
  static reject(reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason)
    })
  }
}

测试结果:

image-20210423201541575

代码优化

对声明类时的冗余代码进行优化,优化完代码如下:

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

class MyPromise {
  constructor(executor) {
    try {
      // 实例化的时候立刻执行传入的这个执行器,并传入 resolve 和 reject
      executor(this.resolve, this.reject)
    } catch (error) {
      // 捕获错误并执行失败回调
      this.reject(error)
    }
  }

  // 初始化状态为 pending
  status = PENDING

  // value 存储成功的结果,reason 存储失败的结果
  value = null
  reason = null

  // 存储成功回调函数
  onFulfilledCallback = []
  // 存储失败回调函数
  onRejectedCallback = []

  resolve = (value) => {
    // 只有当状态为 pending 的时候才执行成功回调
    if (this.status === PENDING) {
      this.status = FULFILLED
      this.value = value
      // 如果存在成功回调,就调用
      while (this.onFulfilledCallback.length) {
        this.onFulfilledCallback.shift()(value)
      }
    }
  }

  reject = (reason) => {
    // 只有当状态为 pending 的时候才执行失败回调
    if (this.status === PENDING) {
      this.status = REJECTED
      this.reason = reason
      // 如果存在成失败回调,就调用
      while (this.onRejectedCallback.length) {
        this.onRejectedCallback.shift()(reason)
      }
    }
  }

  then(onFulfilled, onRejected) {
    // 未指定成功和失败回调时,使用默认函数
    const realOnFulfilled =
      typeof onFulfilled === 'function' ? onFulfilled : (value) => value
    const realOnRejected =
      typeof onRejected === 'function'
        ? onRejected
        : (reason) => {
            throw reason
          }
    // 为了实现链式调用,需要返回一个 MyPromise 对象
    const p = new MyPromise((resolve, reject) => {
      if (this.status === FULFILLED) {
        // 创建一个微任务
        queueMicrotask(() => {
          // 获取成功回调的结果
          try {
            const x = realOnFulfilled(this.value)
            resolvePromise(p, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        })
      } else if (this.status === REJECTED) {
        // 如果传入的实例的状态是失败,则执行失败回调
        queueMicrotask(() => {
          try {
            const x = (realo = OnRejected(this.reason))
            resolvePromise(p, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        })
      } else if (this.status === PENDING) {
        // 如果传入的实例仍是等待中,那么需要先存储成功和失败的回调函数
        this.onFulfilledCallback.push(() => {
          queueMicrotask(() => {
            try {
              const x = onFulfilled(this.value)
              resolvePromise(p, x, resolve, reject)
            } catch (error) {
              reject(error)
            }
          })
        })
        this.onRejectedCallback.push(() => {
          queueMicrotask(() => {
            try {
              const ret = onRejected(this.value)
              resolvePromise(p, ret, resolve, reject)
            } catch (error) {
              reject(error)
            }
          })
        })
      }
    })

    return p
  }

  // resolve 静态方法
  static resolve(parameter) {
    if (parameter instanceof MyPromise) {
      return parameter
    }
    return new MyPromise((resolve) => {
      resolve(parameter)
    })
  }

  // reject 静态方法
  static reject(reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason)
    })
  }
}

function resolvePromise(promise, x, resolve, reject) {
  if (promise === x) {
    // 如果返回值和原 promise 相同,抛出循环调用异常
    return reject(
      new TypeError('Chaining cycle detected for promise #<Promise>')
    )
  }
  if (x instanceof MyPromise) {
    // 如果返回值仍然是一个 MyPromise 实例,则进行链式调用
    x.then(resolve, reject)
  } else {
    // x 是普通值,则直接调用成功回调
    resolve(x)
  }
}

面向 Promise A+ 规范

为了更加符合 Promise A+ 的要求,需要对 resolvePromise 进行优化。

function resolvePromise(promise, x, resolve, reject) {
  if (promise === x) {
    // 如果返回值和原 promise 相同,抛出循环调用异常
    return reject(
      new TypeError('Chaining cycle detected for promise #<Promise>')
    )
  }
  if (typeof x === 'object' || typeof x === 'function') {
    if (x === null) {
      return resolve(x)
    }

    let then
    try {
      then = x.then
    } catch (error) {
      return reject(error)
    }

    if (typeof then === 'function') {
      let called = false
      try {
        then.call(
          x,
          (y) => {
            if (called) return
            called = true
            resolvePromise(promise, y, resolve, reject)
          },
          (r) => {
            if (called) return
            called = true
            reject(r)
          }
        )
      } catch (error) {
        if (called) return
        reject(error)
      }
    } else {
      resolve(x)
    }
  } else {
    // x 是普通值,则直接调用成功回调
    resolve(x)
  }
}

参考资料

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise

https://juejin.cn/post/6945319439772434469#comment