文章

手写Promise全家桶深度解析:从 all 到 any,面试官要你「一次到位」

手写 Promise 核心是状态机不可逆、then 链的值穿透与 thenable 展开,以及 all/race/allSettled/any 四类组合器的语义差异。 面试要一次到位写出并通过边界测试。

手写Promise全家桶深度解析:从 all 到 any,面试官要你「一次到位」

一句话概括

手写 Promise 的核心不是背 A+ 规范的 100 行代码,而是理解状态机的不可逆、then 链的值穿透和 thenable 展开、以及 all/race/allSettled/any 四种组合器各自「最先成功的/最先失败的/全成功的/全都完成的」微妙差异。

核心知识点

1. 最简 Promise:状态机 + 回调队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
const PENDING = 'pending', FULFILLED = 'fulfilled', REJECTED = 'rejected'

class MyPromise {
  state = PENDING; value = undefined; reason = undefined
  onFulfilled = []; onRejected = []

  constructor(executor) {
    const resolve = (val) => {
      if (this.state !== PENDING) return
      this.state = FULFILLED; this.value = val
      this.onFulfilled.forEach(fn => fn())
    }
    const reject = (err) => {
      if (this.state !== PENDING) return
      this.state = REJECTED; this.reason = err
      this.onRejected.forEach(fn => fn())
    }
    try { executor(resolve, reject) } catch (e) { reject(e) }
  }

  then(onFulfilled, onRejected) {
    const p2 = new MyPromise((resolve, reject) => {
      const handle = (fn, value, isReject) => {
        queueMicrotask(() => {
          try {
            if (typeof fn !== 'function') {
              isReject ? reject(value) : resolve(value) // 值穿透
            } else {
              this._resolvePromise(p2, fn(value), resolve, reject)
            }
          } catch (e) { reject(e) }
        })
      }
      if (this.state === FULFILLED) handle(onFulfilled, this.value, false)
      else if (this.state === REJECTED) handle(onRejected, this.reason, true)
      else { this.onFulfilled.push(() => handle(onFulfilled, this.value, false)); this.onRejected.push(() => handle(onRejected, this.reason, true)) }
    })
    return p2
  }

  _resolvePromise(p2, x, resolve, reject) {
    if (p2 === x) return reject(new TypeError('循环引用'))
    if (x instanceof MyPromise) { x.then(resolve, reject); return }
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
      let then, called = false
      try { then = x.then } catch (e) { reject(e); return }
      if (typeof then === 'function') {
        try {
          then.call(x, y => { if (!called) { called = true; this._resolvePromise(p2, y, resolve, reject) } },
            r => { if (!called) { called = true; reject(r) } })
        } catch (e) { if (!called) reject(e) }
        return
      }
    }
    resolve(x)
  }

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

  static resolve(v) { return v instanceof MyPromise ? v : new MyPromise(r => r(v)) }
  static reject(r) { return new MyPromise((_, rj) => rj(r)) }
}

2. Promise.all:「全部成功 or 快速失败」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MyPromise.all = function (iterable) {
  return new MyPromise((resolve, reject) => {
    const arr = Array.from(iterable)
    if (!arr.length) return resolve([])
    const results = new Array(arr.length)
    let remaining = arr.length
    arr.forEach((item, i) => {
      MyPromise.resolve(item).then(
        v => { results[i] = v; if (--remaining === 0) resolve(results) },
        reject  // 任一失败,立即 reject
      )
    })
  })
}

3. Promise.race:「第一个 settled」

1
2
3
4
5
6
7
8
9
10
MyPromise.race = function (iterable) {
  return new MyPromise((resolve, reject) => {
    for (const item of Array.from(iterable)) {
      MyPromise.resolve(item).then(resolve, reject)
    }
  })
}

// ⚠️ 陷阱:空数组返回永远 pending 的 Promise
// MyPromise.race([]) → 永远不会 resolve 或 reject

4. Promise.allSettled:「全部完成,一个不落」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MyPromise.allSettled = function (iterable) {
  return new MyPromise(resolve => {
    const arr = Array.from(iterable)
    if (!arr.length) return resolve([])
    const results = new Array(arr.length)
    let done = 0
    arr.forEach((item, i) => {
      MyPromise.resolve(item).then(
        v => { results[i] = { status: 'fulfilled', value: v }; if (++done === arr.length) resolve(results) },
        r => { results[i] = { status: 'rejected', reason: r }; if (++done === arr.length) resolve(results) }
      )
    })
  })
}

5. Promise.any:「第一个成功 or 全部拒绝」

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MyPromise.any = function (iterable) {
  return new MyPromise((resolve, reject) => {
    const arr = Array.from(iterable)
    if (!arr.length) return reject(new AggregateError([], 'All promises were rejected'))
    const errors = new Array(arr.length)
    let failed = 0
    arr.forEach((item, i) => {
      MyPromise.resolve(item).then(
        resolve,
        r => { errors[i] = r; if (++failed === arr.length) reject(new AggregateError(errors, 'All promises were rejected')) }
      )
    })
  })
}

四方法对比速记:

方法何时 resolve何时 reject
all全部成功任意一个失败
race任意一个 settled(无论成功/失败)同左
allSettled全部 settled永不 reject
any任意一个成功全部失败(AggregateError)

其实你每天都在用

  • 页面并行请求 — Promise.all([fetchUser(), fetchOrders(), fetchCart()]),三个接口一起发,全部回来才渲染页面。核心接口一般还包 Promise.race 加超时
  • 竞速加载 — Promise.race([cdnA.fetch(), cdnB.fetch()]),哪个 CDN 先返回就用哪个,另一个自动丢弃
  • 多选源数据 — Promise.any([apiV1.get(), apiV2.get()]),有一个成功就行,用于主备接口自动切换
  • 多文件上传 — Promise.allSettled(uploadTasks),5 个文件上传,哪怕 2 个失败也要知道哪 2 个,不能整体 reject
  • async/await 底层 — 你写的 await fetch('/api') 本质就是 fetch('/api').then(result => ...),Babel 编译后就是 Promise 链

常见误解(FAQ)

❌ 误区:「Promise.race([]) 会立即 reject」

错了——返回一个永远 pending 的 Promise。因为 iterable 为空时 for 循环不执行任何语句,resolve 和 reject 都从未被调用。这是面试经典陷阱。

❌ 误区:「Promise.all 的结果顺序和输入顺序一致」

这个说法是对的——但原理不是「等所有完成再排序」。index 是在 forEach 闭包中捕获的 (item, i),每个 Promise resolve 时写入 results[i],天然保持输入顺序。

❌ 误区:「catch 就是 then(null, onRejected) 的语法糖」

基本正确但不完全。p.catch(fn) 等于 p.then(undefined, fn),但 p.then(resolveFn, rejectFn) 中的 rejectFn 只能捕获 p 的 rejection,不能捕获 resolveFn 内的异常;而 .catch 放在最后作为兜底可以捕获前面所有 then 的异常。所以 .then(fn1, fn2) 和 .then(fn1).catch(fn2) 不一样。

❌ 误区:「Promise.resolve(thenable) 直接返回 thenable」

Promise.resolve() 会递归展开 thenable——如果你传一个有 .then 方法的对象,它会调用该 then 方法并等待其 settle。这是为了兼容已有的类 Promise 库(如 jQuery Deferred)。

一句话总结

Promise 是 microtask 调度 + 状态机 + 回调队列的优雅组合——面试时你不是在写代码,而是在向面试官证明你理解了浏览器事件循环中「微任务队列被清空前,宏任务永远不会执行」这条铁律。

本文由作者按照 CC BY 4.0 进行授权