← 返回题目列表

如何手写一个简化版 Promise?

高频 困难 第 17 / 27 题 更新于 2026/07/28
手写代码Promise异步JavaScript

简化版

手写 Promise 的核心是状态机、反应队列和链式决议。状态只有 pending、fulfilled、rejected,且只能从 pending 单向改变;then 必须返回新 Promise,回调要进入微任务;回调返回普通值、新 Promise 或任意 thenable 时,新 Promise 的状态都要按决议过程接管。

详细版

最低限度要实现:执行器同步执行、resolve/reject 只接受第一次调用、pending 时保存回调、settled 后调用 then 仍异步执行、缺省处理器透传值或错误,以及 then 返回新实例。

const p = new MiniPromise(resolve => resolve(1))
const next = p.then(value => value + 1)
next.then(console.log) // 异步输出 2

真正难点不是三个状态常量,而是 Promise Resolution Procedure:禁止新 Promise 用自身完成;读取 then 可能抛错;thenable 可能同时调用成功和失败,必须只认第一次。教学实现也应明确,它没有覆盖原生 Promise 的全部构造器泛化、species、拒绝跟踪和静态组合方法。

完整版教学

一、状态机只是第一层

当前状态操作结果
pendingfulfill(value)变为 fulfilled,结果固定
pendingreject(reason)变为 rejected,原因固定
fulfilled/rejected再次决议忽略

“resolved”不完全等于“fulfilled”。一个 Promise resolve 到另一个 pending Promise 后,已经锁定要跟随它,自己仍可能暂时 pending,但后续 reject 已不能改变结果。

面试实现若在 resolve(thenable) 后仍允许同步 reject() 抢先,就没有做到“第一次决议锁定”。状态何时落定与决议权何时消耗,是两个层次。

二、执行器同步,反应回调异步

console.log('A')
new Promise(resolve => {
  console.log('B')
  resolve()
}).then(() => console.log('C'))
console.log('D')
// A、B、D、C

构造器中的 executor 立即同步执行;then 回调必须排到当前同步代码之后。浏览器和 Node 中原生 Promise 使用宿主 Promise Job 队列,教学版可用 queueMicrotask 近似调度,不能用同步调用,也不应把 setTimeout 宏任务说成完全等价。

三、一个可运行的教学实现

下面实现覆盖核心链式行为,但有意省略静态组合方法、species 和宿主拒绝跟踪:

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

class MiniPromise {
  constructor(executor) {
    if (typeof executor !== 'function') throw new TypeError('executor required')

    this.state = PENDING
    this.value = undefined
    this.reactions = []
    let alreadyResolved = false

    const resolve = value => {
      if (alreadyResolved) return
      alreadyResolved = true
      this.resolveValue(value)
    }
    const reject = reason => {
      if (alreadyResolved) return
      alreadyResolved = true
      this.rejectInternal(reason)
    }

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

  fulfill(value) {
    if (this.state !== PENDING) return
    this.state = FULFILLED
    this.value = value
    this.flush()
  }

  rejectInternal(reason) {
    if (this.state !== PENDING) return
    this.state = REJECTED
    this.value = reason
    this.flush()
  }

  resolveValue(value) {
    if (value === this) {
      this.rejectInternal(new TypeError('self resolution'))
      return
    }

    if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
      this.fulfill(value)
      return
    }

    let then
    try {
      then = value.then
    } catch (error) {
      this.rejectInternal(error)
      return
    }

    if (typeof then !== 'function') {
      this.fulfill(value)
      return
    }

    queueMicrotask(() => {
      let called = false
      try {
        Reflect.apply(then, value, [
          nextValue => {
            if (called) return
            called = true
            this.resolveValue(nextValue)
          },
          reason => {
            if (called) return
            called = true
            this.rejectInternal(reason)
          },
        ])
      } catch (error) {
        if (!called) this.rejectInternal(error)
      }
    })
  }

  then(onFulfilled, onRejected) {
    return new MiniPromise((resolve, reject) => {
      const reaction = () => {
        queueMicrotask(() => {
          const handler = this.state === FULFILLED ? onFulfilled : onRejected
          if (typeof handler !== 'function') {
            if (this.state === FULFILLED) resolve(this.value)
            else reject(this.value)
            return
          }
          try {
            resolve(handler(this.value))
          } catch (error) {
            reject(error)
          }
        })
      }

      if (this.state === PENDING) this.reactions.push(reaction)
      else reaction()
    })
  }

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

  flush() {
    const reactions = this.reactions
    this.reactions = []
    for (const reaction of reactions) reaction()
  }
}

回调数组在 flush 时先被替换为空数组,避免长期持有闭包,也让执行过程中的新增反应按 settled 分支独立调度。

四、then 为什么一定返回新 Promise

若返回原对象,下面链条无法让每一段拥有独立结果:

const p = new MiniPromise(resolve => resolve(1))
const a = p.then(x => x + 1)
const b = p.then(x => x + 10)

a 应完成为 2,b 应完成为 11,二者不能改写 p。回调抛错时只拒绝对应的新 Promise;返回值则交给新 Promise 的 resolve 过程,因此返回 Promise 或 thenable 都能被吸收。

缺省成功处理器相当于 value => value,缺省失败处理器相当于 reason => { throw reason },所以值和错误都能沿链条穿透,直到遇到真正处理它的回调。

五、thenable 决议的四道防线

const hostile = {
  then(resolve, reject) {
    resolve(1)
    reject(new Error('ignored'))
    resolve(2)
  },
}

可靠过程至少包含:

  1. 新 Promise 不能用自身完成,否则以 TypeError 拒绝。
  2. then 属性只能读取一次,因为 getter 可能有副作用或抛错。
  3. 调用 thenable 时绑定其自身为 this
  4. 局部 called 标志保证 resolve、reject、抛错三条路径只认第一次。

这里的 called 与构造器中的 alreadyResolved 解决不同问题:前者防恶意 thenable 多次回调,后者防 executor 同时调用 resolve 和 reject。

六、与原生 Promise 的差距

能力本文实现原生 Promise
核心状态与链式 then支持支持
通用 thenable 吸收支持主要路径规范完整支持
finally、静态组合方法未实现完整支持
子类与 Symbol.species未实现有规范规则
未处理拒绝跟踪未实现由宿主环境跟踪
内部品牌和跨 realm 细节未实现引擎内部实现

另外,原生 resolve thenable 会创建专门的 Promise Job,循环 thenable、代理、realm 和错误顺序都有精细规则。本文适合解释机制和常规用例,不是替换浏览器原生 Promise 的生产 polyfill。

七、常见误区与追问

  • 误区:调用 resolve 就一定立刻变成 fulfilled。 resolve 到 pending Promise 或 thenable 时会锁定并等待其结果。
  • 误区:pending 时异步,settled 后再 then 就同步。 无论注册早晚,then 回调都必须异步调度。
  • 误区:只识别 value instanceof MiniPromise 就够了。 决议过程要吸收任何拥有可调用 then 的对象或函数。
  • 追问:为什么读取 then 要放在 try/catch? 它可能是会抛错的 getter,读取失败应拒绝新 Promise。
  • 追问:回调返回自身链条会怎样? 新 Promise 最终解析到自己,必须以 TypeError 拒绝,不能无限等待。
  • 追问:为什么不用 setTimeout 调度回调? 它进入宏任务队列,和原生 Promise 微任务的相对顺序不同。
  • 追问:catch 是独立机制吗? 不是,等价于 then(undefined, onRejected) 的便捷方法。

八、加强记忆

  1. 状态:pending 只能单向进入 fulfilled 或 rejected。
  2. 锁定:第一次 resolve/reject 消耗决议权,即使 thenable 尚未落定。
  3. 异步:executor 同步,then 反应进入微任务。
  4. 链式:每次 then 返回新 Promise,结果互不污染。
  5. 吸收:读一次 then、防自身循环、防重复回调、捕获异常。
  6. 边界:教学类解释核心机制,不等同原生 Promise 的完整规范实现。