手写Promise全家桶深度解析:从 all 到 any,面试官要你「一次到位」
手写 Promise 核心是状态机不可逆、then 链的值穿透与 thenable 展开,以及 all/race/allSettled/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 调度 + 状态机 + 回调队列的优雅组合——面试时你不是在写代码,而是在向面试官证明你理解了浏览器事件循环中「微任务队列被清空前,宏任务永远不会执行」这条铁律。