返回首页

手写 Promise:从状态机到 then 链式调用一步步实现

会用 Promise 和懂 Promise 是两回事。这篇从三个状态、then 的注册与触发讲起,一步步手写一个符合 Promise/A+ 核心语义的实现,把异步、链式调用、值穿透、错误冒泡这些机制彻底讲明白。

21 约 5 分钟 · 7542 字 前端

Promise 几乎天天在用,但真要你手写一个,很多人就卡住了。其实它的核心机制并不神秘:一个状态机 + 一组回调队列 + 链式调用

这篇我们从零实现一个符合 Promise/A+ 核心语义的版本,把它内部到底怎么运转讲清楚。看完你会对 then 链、值穿透、错误冒泡有完全不同的理解。

先明确 Promise 的三个规则

实现之前,先把规则定下来,这是后面所有代码的依据:

  1. 三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)。
  2. 状态只能改变一次:pending → fulfilledpending → rejected,一旦改变就锁定,不可逆。
  3. then 接收两个回调:onFulfilledonRejected,状态变化时执行对应的那个。

第一版:基础状态机

先实现状态流转。构造函数接收一个 executor,立即执行,并给它传 resolvereject 两个函数:

class MyPromise {
  constructor(executor) {
    this.state = 'pending'
    this.value = undefined      // 成功的值
    this.reason = undefined     // 失败的原因

    const resolve = (value) => {
      if (this.state !== 'pending') return // 状态只能改一次
      this.state = 'fulfilled'
      this.value = value
    }

    const reject = (reason) => {
      if (this.state !== 'pending') return
      this.state = 'rejected'
      this.reason = reason
    }

    try {
      executor(resolve, reject) // executor 同步执行
    } catch (err) {
      reject(err) // executor 里抛错,直接 reject
    }
  }
}

这一版能记录状态和值,但还没法注册回调。

第二版:支持 then(含异步)

then 要根据当前状态执行对应回调。难点在于:resolve 可能是异步调用的(比如在 setTimeout 里),这时候调用 then,状态还是 pending,回调没法立刻执行。

解决办法:pending 时把回调先存起来,等 resolve / reject 时再拿出来执行。这就是"回调队列"。

class MyPromise {
  constructor(executor) {
    this.state = 'pending'
    this.value = undefined
    this.reason = undefined
    this.onFulfilledCbs = [] // 成功回调队列
    this.onRejectedCbs = []  // 失败回调队列

    const resolve = (value) => {
      if (this.state !== 'pending') return
      this.state = 'fulfilled'
      this.value = value
      this.onFulfilledCbs.forEach((fn) => fn()) // 状态确定,把存好的回调都执行
    }

    const reject = (reason) => {
      if (this.state !== 'pending') return
      this.state = 'rejected'
      this.reason = reason
      this.onRejectedCbs.forEach((fn) => fn())
    }

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

  then(onFulfilled, onRejected) {
    if (this.state === 'fulfilled') {
      onFulfilled(this.value)
    } else if (this.state === 'rejected') {
      onRejected(this.reason)
    } else {
      // pending:先存起来,等状态确定再执行
      this.onFulfilledCbs.push(() => onFulfilled(this.value))
      this.onRejectedCbs.push(() => onRejected(this.reason))
    }
  }
}

现在它已经能处理异步了:

new MyPromise((resolve) => {
  setTimeout(() => resolve('ok'), 1000)
}).then((v) => console.log(v)) // 1 秒后打印 ok

第三版:链式调用与值穿透

这是 Promise 最精髓、也最难的部分。then返回一个新的 Promise,才能链式调用,而且要处理几种情况:

  1. 回调返回普通值 → 新 Promise 用这个值 resolve。
  2. 回调返回一个 Promise → 新 Promise 要等它,跟随它的结果。
  3. 回调抛错 → 新 Promise reject。
  4. 回调没传(值穿透) → 把值/原因往后传。
then(onFulfilled, onRejected) {
  // 值穿透:onFulfilled / onRejected 不是函数时,给个默认实现把值往后传
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (v) => v
  onRejected = typeof onRejected === 'function' ? onRejected : (e) => { throw e }

  // then 返回新 Promise,这是链式调用的关键
  const promise2 = new MyPromise((resolve, reject) => {
    const handleFulfilled = () => {
      // 用 setTimeout 模拟微任务,保证回调异步执行(Promise/A+ 要求)
      setTimeout(() => {
        try {
          const x = onFulfilled(this.value)
          resolvePromise(promise2, x, resolve, reject) // 处理返回值
        } catch (err) {
          reject(err)
        }
      })
    }

    const handleRejected = () => {
      setTimeout(() => {
        try {
          const x = onRejected(this.reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (err) {
          reject(err)
        }
      })
    }

    if (this.state === 'fulfilled') handleFulfilled()
    else if (this.state === 'rejected') handleRejected()
    else {
      this.onFulfilledCbs.push(handleFulfilled)
      this.onRejectedCbs.push(handleRejected)
    }
  })

  return promise2
}

核心是那个 resolvePromise,它负责处理"回调返回值 x":如果 x 是个 Promise(或 thenable),就跟随它;否则直接 resolve。

function resolvePromise(promise2, x, resolve, reject) {
  // 防止循环引用:then 返回了自己
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected'))
  }

  // x 是对象或函数,可能是 thenable
  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    let called = false // 防止 resolve / reject 被重复调用
    try {
      const then = x.then
      if (typeof then === 'function') {
        // x 是 thenable,跟随它的状态
        then.call(
          x,
          (y) => {
            if (called) return
            called = true
            resolvePromise(promise2, y, resolve, reject) // 递归展开
          },
          (r) => {
            if (called) return
            called = true
            reject(r)
          },
        )
      } else {
        resolve(x) // 普通对象,直接 resolve
      }
    } catch (err) {
      if (called) return
      called = true
      reject(err)
    }
  } else {
    resolve(x) // x 是普通值
  }
}

到这里,核心的链式调用就完成了:

new MyPromise((resolve) => resolve(1))
  .then((v) => v + 1)               // 返回普通值
  .then((v) => new MyPromise((r) => r(v * 10))) // 返回 Promise
  .then((v) => console.log(v))      // 20

值穿透也能用了:

new MyPromise((resolve) => resolve('hi'))
  .then()                            // 没传回调,值穿透
  .then((v) => console.log(v))       // 'hi'

补上 catch、resolve、reject、all

有了 then,其它方法都是它的语法糖或简单封装。

catch 就是只传第二个参数的 then:

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

静态的 resolve / reject:

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

static reject(reason) {
  return new MyPromise((_, reject) => reject(reason))
}

Promise.all:全部成功才成功,按顺序收集结果,任一失败则整体失败:

static all(promises) {
  return new MyPromise((resolve, reject) => {
    const results = []
    let count = 0

    promises.forEach((p, index) => {
      // 用 MyPromise.resolve 包一层,兼容传入非 Promise 值
      MyPromise.resolve(p).then(
        (value) => {
          results[index] = value // 按原顺序放,不是按完成顺序
          count++
          if (count === promises.length) resolve(results)
        },
        (reason) => reject(reason), // 一个失败,整体失败
      )
    })

    if (promises.length === 0) resolve([])
  })
}

Promise.race:谁先有结果就用谁的:

static race(promises) {
  return new MyPromise((resolve, reject) => {
    promises.forEach((p) => {
      MyPromise.resolve(p).then(resolve, reject) // 第一个改变状态的说了算
    })
  })
}

几个关键点回顾

手写过一遍,这几个机制就真正理解了:

  • 状态不可逆:resolve / reject 开头那句 if (this.state !== 'pending') return,保证状态只变一次。
  • 回调队列:pending 时回调存进数组,状态确定时统一触发——这是支持异步的关键。
  • then 返回新 Promise:链式调用的根本,每个 then 产生一个新的 Promise。
  • resolvePromise:处理回调返回值,返回 Promise 就跟随它,这是链式能"等待"的原因。
  • 值穿透:then 不传回调时,用默认函数把值/错误往后传,所以 .then().then(fn) 也能拿到值。
  • 错误冒泡:某一环 reject 或抛错,会一路往后传,直到被某个 onRejected / catch 捕获。

说明:真实 Promise 的回调是放进微任务队列的,这里用 setTimeout 模拟成宏任务只是为了演示"异步执行"这个语义。要严格符合微任务时序,可以用 queueMicrotask 替换 setTimeout

总结

Promise 看似复杂,拆开看就是三块:

  1. 一个状态机:三状态、只能变一次。
  2. 两个回调队列:解决异步注册的回调延后执行。
  3. then 返回新 Promise + resolvePromise:撑起链式调用、值穿透和错误冒泡。

把这套亲手写一遍,你对 async/await(它本质是 Promise 的语法糖)、对事件循环里的微任务、对各种链式调用的执行顺序,都会有质的理解。这也是为什么"手写 Promise"一直是检验 JS 功底的经典题目。