手写:Promise类深度解析
从零手写完整 Promise 类,深入理解 Promise 状态机、then 链式调用、微任务调度等核心机制,彻底掌握异步编程底层原理。
一句话概括
手写 Promise 类是理解异步编程底层机制的最佳路径——通过实现状态机、then 链式调用、微任务调度,你将真正掌握 Promise 的工作原理,而不只是会用 API。
背景
Promise 是 JavaScript 异步编程的基石,ES6 正式引入规范(Promises/A+)。在此之前,回调地狱(Callback Hell)让异步代码难以维护。Promise 通过状态机 + 链式调用,将异步流程变得可读、可组合。
面试中,手写 Promise 是高频考题,考察候选人对以下内容的理解:
- 状态机设计
- 微任务调度(queueMicrotask / MutationObserver)
- then 的链式调用与值穿透
- 错误冒泡机制
概念与定义
Promise 状态机
Promise 有三种状态,且状态只能单向流转:
1
2
pending → fulfilled(resolve 触发)
pending → rejected(reject 触发)
一旦状态确定,不可再变更。
Promises/A+ 规范核心要点
then方法接收两个可选回调:onFulfilled和onRejectedthen必须返回一个新的 Promise- 回调必须异步执行(微任务)
- 若回调返回值是 thenable,需要递归解析(Resolution Procedure)
最小示例
1
2
3
4
5
6
7
8
9
10
11
12
const p = new MyPromise((resolve, reject) => {
setTimeout(() => resolve('hello'), 100);
});
p.then(val => {
console.log(val); // 'hello'
return val + ' world';
}).then(val => {
console.log(val); // 'hello world'
}).catch(err => {
console.error(err);
});
核心知识点拆解
1. 状态与值的存储
1
2
3
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
每个 Promise 实例需要存储:
state:当前状态value:fulfilled 时的值reason:rejected 时的原因onFulfilledCallbacks:待执行的成功回调队列onRejectedCallbacks:待执行的失败回调队列
2. resolve / reject 的实现
1
2
3
4
5
6
7
resolve(value) {
if (this.state !== PENDING) return; // 状态不可逆
this.state = FULFILLED;
this.value = value;
// 通知所有等待中的回调
this.onFulfilledCallbacks.forEach(fn => fn(value));
}
3. then 的链式调用
then 返回新 Promise,新 Promise 的状态由回调的返回值决定:
- 返回普通值 → 新 Promise fulfilled
- 抛出异常 → 新 Promise rejected
- 返回 Promise/thenable → 递归解析
4. resolvePromise(Resolution Procedure)
这是 Promises/A+ 规范最复杂的部分,处理 then 回调返回值为 thenable 的情况,防止循环引用。
5. 微任务调度
回调必须异步执行,使用 queueMicrotask(或降级到 setTimeout):
1
2
3
4
5
6
7
8
queueMicrotask(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
实战案例:完整手写实现
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
constructor(executor) {
this.state = PENDING;
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
// 如果 resolve 的值本身是 Promise,需要等待其完成
if (value instanceof MyPromise) {
value.then(resolve, reject);
return;
}
if (this.state !== PENDING) return;
this.state = FULFILLED;
this.value = value;
this.onFulfilledCallbacks.forEach(fn => fn());
};
const reject = (reason) => {
if (this.state !== PENDING) return;
this.state = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
};
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulfilled, onRejected) {
// 值穿透处理
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };
const promise2 = new MyPromise((resolve, reject) => {
const fulfilledMicrotask = () => {
queueMicrotask(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
};
const rejectedMicrotask = () => {
queueMicrotask(() => {
try {
const x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
});
};
if (this.state === FULFILLED) {
fulfilledMicrotask();
} else if (this.state === REJECTED) {
rejectedMicrotask();
} else {
// pending 状态,将回调存入队列
this.onFulfilledCallbacks.push(fulfilledMicrotask);
this.onRejectedCallbacks.push(rejectedMicrotask);
}
});
return promise2;
}
catch(onRejected) {
return this.then(null, onRejected);
}
finally(callback) {
return this.then(
value => MyPromise.resolve(callback()).then(() => value),
reason => MyPromise.resolve(callback()).then(() => { throw reason; })
);
}
// 静态方法
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 Resolution Procedure
* 处理 then 回调返回值,支持 thenable 递归解析
*/
function resolvePromise(promise2, x, resolve, reject) {
// 防止循环引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
// x 是 Promise 实例
if (x instanceof MyPromise) {
x.then(
y => resolvePromise(promise2, y, resolve, reject),
reject
);
return;
}
// x 是 thenable 对象或函数
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
let called = false; // 防止多次调用
try {
const then = x.then;
if (typeof then === 'function') {
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);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
return;
}
// x 是普通值
resolve(x);
}
底层原理
微任务 vs 宏任务
Promise 的回调必须是微任务,这是 Promises/A+ 规范的要求。微任务在当前宏任务执行完毕后、下一个宏任务开始前执行,保证了 Promise 回调的及时性。
1
2
3
4
5
宏任务(script)
└─ 同步代码执行
└─ 微任务队列清空(Promise.then 回调)
└─ 渲染
└─ 下一个宏任务(setTimeout 等)
为什么 then 必须返回新 Promise?
如果 then 返回同一个 Promise,会导致状态无法确定(循环依赖)。返回新 Promise 保证了链式调用的独立性和可组合性。
resolvePromise 的 called 标志
防止 thenable 对象的 resolve/reject 被多次调用(某些不规范的 thenable 实现可能会这样做),保证状态只变更一次。
高频面试题解析
Q1:Promise 的状态可以逆转吗?
不可以。 一旦从 pending 变为 fulfilled 或 rejected,状态就固定了。这是 Promise 可靠性的基础。
Q2:then 的回调为什么必须异步执行?
保证行为一致性。无论 Promise 是同步 resolve 还是异步 resolve,then 的回调都在当前同步代码执行完毕后才执行,避免了同步/异步行为不一致的问题。
Q3:值穿透是什么?
当 then 的参数不是函数时,会将上一个 Promise 的值直接传递给下一个 then:
1
2
3
Promise.resolve(1)
.then(2) // 非函数,值穿透
.then(val => console.log(val)); // 输出 1
实现方式:onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val
Q4:catch 和 then(null, onRejected) 有什么区别?
catch(fn) 等价于 then(null, fn),但有一个细微差别:
1
2
3
4
5
promise.then(onFulfilled, onRejected);
// onRejected 无法捕获 onFulfilled 内部抛出的错误
promise.then(onFulfilled).catch(onRejected);
// catch 可以捕获 onFulfilled 内部的错误
推荐使用 .catch() 而非 then 的第二个参数。
Q5:如何实现 Promise 的取消?
原生 Promise 不支持取消,常见方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 方案一:包装一个永不 resolve 的 Promise
function cancelable(promise) {
let cancel;
const wrapper = new Promise((resolve, reject) => {
cancel = reject;
promise.then(resolve, reject);
});
wrapper.cancel = cancel;
return wrapper;
}
// 方案二:使用 AbortController(fetch 场景)
const controller = new AbortController();
fetch(url, { signal: controller.signal });
controller.abort(); // 取消请求
Q6:Promise.resolve() 传入 Promise 实例会怎样?
1
2
const p = new Promise(resolve => resolve(1));
Promise.resolve(p) === p; // true,直接返回原 Promise
但 new Promise(resolve => resolve(p)) 会等待 p 完成,这是两种不同的行为。
总结与扩展
核心要点回顾
| 知识点 | 关键细节 |
|---|---|
| 状态机 | pending → fulfilled/rejected,不可逆 |
| then 返回值 | 必须是新 Promise |
| 微任务调度 | queueMicrotask / MutationObserver |
| resolvePromise | 处理 thenable,防循环引用 |
| 值穿透 | 非函数参数时直接传递值 |
扩展阅读
- Promises/A+ 规范:https://promisesaplus.com/
- Promise.all 手写:下周一(第4周)将深入实现
- async/await 原理:基于 Generator + Promise 的语法糖,已在第3周周二讲解
- 微任务调度器:queueMicrotask 的 polyfill 实现
学习路径
1
2
3
4
5
6
7
8
9
手写 Promise
↓
手写 Promise.all / race / allSettled
↓
手写并发调度器(基于 Promise)
↓
理解 async/await 编译产物
↓
掌握完整异步编程体系
💡 面试建议:手写 Promise 时,先写出骨架(状态机 + resolve/reject + then 返回新 Promise),再补充 resolvePromise 的 thenable 处理。不要一开始就陷入细节,先让基本链式调用跑通,再处理边界情况。