Appearance
Promise
Promise 的 .then() 回调之所以先于 setTimeout 执行,不是因为“Promise 更快”,而是因为它们进入的是不同的调度队列:microtask queue 和 macrotask queue。理解这一区分,比单独记忆 Promise API 更关键。
事件循环中的 Promise:microtask 的调度机制
JS 引擎的事件循环有两条队列:
text
Macrotask Queue (Task Queue)
├── setTimeout / setInterval
├── I/O
├── UI rendering
└── postMessage / MessageChannel
Microtask Queue
├── Promise.then / .catch / .finally
├── queueMicrotask
├── MutationObserver
└── async/await(本质上 await 之后的代码 = .then())关键规则:每个 macrotask 执行完毕后,在进入下一个 macrotask 之前,必须清空 microtask queue。
这个规则直接决定了以下代码的执行顺序:
js
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出:1 → 4 → 3 → 2
// 解释:
// 1: 同步代码
// 4: 同步代码
// → 当前 macrotask 结束,开始清空 microtask queue
// 3: Promise.then() 微任务
// → microtask queue 清空,进入下一个 macrotask
// 2: setTimeout 回调queueMicrotask API 是 Promise 内部用来调度 .then() 回调的底层机制。在 V8 中,Promise.prototype.then 的 C++ 实现调用 EnqueueMicrotask,将回调注册到当前执行上下文的微任务队列中。
为什么不用 setTimeout(fn, 0) 来实现异步?因为那会把回调放入 macrotask queue,排在当前队列中所有已有的 macrotask 之后,且在每个 macrotask 之间浏览器可能插入渲染帧——延迟不可控。Microtask 在当前 macrotask 结束后的第一时间执行,且"一口气"清空整个队列,中间不插入渲染。
这种"一口气清空"的行为也是 Promise 的一个潜在风险:如果你在一个 .then() 回调中递归地创建新的 Promise 并链式调用,microtask queue 会不断增长,导致主线程被 microtask 持续占用,浏览器无法插入渲染帧——页面看起来就像卡死了。
js
// ⚠️ microtask 风暴:浏览器无法渲染
function microtaskStorm() {
Promise.resolve().then(() => {
doSomeWork();
microtaskStorm(); // 递归注册微任务 → microtask queue 永远不会空
});
}Promise/A+ 规范中的关键边界条件
手写 Promise 如果不读规范,很容易实现出"看起来对但不完全对"的行为。Promise/A+ 规范中有几个容易出错的点:
1. Promise Resolution Procedure(§2.3)
当 .then() 的 onFulfilled 返回一个值 x 时,规范定义了 [[Resolve]](promise, x) 过程:
- 如果
x和promise是同一个对象 → reject TypeError(防止循环引用) - 如果
x是一个 Promise → 采用x的状态和值 - 如果
x是一个对象或函数(且不为 null),且具有then方法 → 将其视为 thenable,调用x.then(resolve, reject) - 否则 → 用
xresolve promise
这个 thenable 处理是 Promise 实现中最容易遗漏的细节。它允许不同 Promise 库之间互操作——一个库的 Promise 可以 .then() 另一个库的 thenable 对象。
2. 值的穿透(Value Penetration)
当 .then() 的 onFulfilled 不是函数时,规范要求"沿链向下传递值":
js
Promise.resolve(42)
.then(null) // onFulfilled 不是函数 → 值穿透
.then(undefined) // 同上
.then(v => console.log(v)); // 输出 42这不是"跳过 null",而是规范明确规定的行为:如果 onFulfilled 不是函数,则用当前值 resolve 新 Promise。
3. 错误穿透
当 .then() 的 onRejected 不是函数时,错误沿链向下传播:
js
Promise.reject('error')
.then(v => v) // onRejected 不是函数 → 错误穿透
.then(v => v) // 同上
.catch(e => console.log(e)); // 输出 'error'4. thenable 的鸭子类型检测
规范用"是否存在 .then 方法"来判断一个对象是否是 Promise-like,而不是用 instanceof Promise。这意味着任何带 .then() 方法的对象都可以被 Promise 系统消费:
js
const thenable = {
then(resolve, reject) {
setTimeout(() => resolve('ok'), 1000);
}
};
Promise.resolve(thenable).then(v => console.log(v)); // 1 秒后输出 'ok'这个设计选择是故意的——它让 ES6 Promise 和之前存在的 Promise 库(Bluebird、Q、jQuery Deferred)能够互操作。代价是任何恰好有 .then 属性的对象可能被误认为是 Promise,但实际工程中极少遇到这种冲突。
手写 Promise 实现(TypeScript 完整版)
以下实现覆盖了规范的核心要求:状态不可逆、queueMicrotask 调度、Resolution Procedure、thenable 检测、值穿透、完整的组合子。
ts
type Resolve<T> = (value: T | PromiseLike<T>) => void;
type Reject = (reason?: any) => void;
type Executor<T> = (resolve: Resolve<T>, reject: Reject) => void;
type OnFulfilled<T, R> = ((value: T) => R | PromiseLike<R>) | undefined | null;
type OnRejected<R> = ((reason: any) => R | PromiseLike<R>) | undefined | null;
enum State { PENDING, FULFILLED, REJECTED }
interface PromiseLike<T> {
then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): PromiseLike<TResult1 | TResult2>;
}
class MyPromise<T> {
private state: State = State.PENDING;
private value!: T;
private reason!: any;
private onFulfilledCallbacks: Array<() => void> = [];
private onRejectedCallbacks: Array<() => void> = [];
constructor(executor: Executor<T>) {
const resolve: Resolve<T> = (value) => {
if (this.state !== State.PENDING) return;
// Resolution Procedure: 如果 resolve 的值是一个 thenable,需要等待它
if (value instanceof MyPromise || (value !== null && typeof value === 'object' && typeof (value as any).then === 'function')) {
(value as PromiseLike<T>).then(resolve, reject);
return;
}
this.state = State.FULFILLED;
this.value = value as T;
// 规范要求 .then() 回调异步执行 → queueMicrotask
this.onFulfilledCallbacks.forEach(cb => queueMicrotask(cb));
};
const reject: Reject = (reason) => {
if (this.state !== State.PENDING) return;
this.state = State.REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(cb => queueMicrotask(cb));
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then<TResult1 = T, TResult2 = never>(
onFulfilled?: OnFulfilled<T, TResult1>,
onRejected?: OnRejected<TResult2>
): MyPromise<TResult1 | TResult2> {
return new MyPromise<TResult1 | TResult2>((resolve, reject) => {
const handleFulfilled = () => {
try {
if (typeof onFulfilled === 'function') {
const result = onFulfilled(this.value);
result instanceof MyPromise
? result.then(resolve as any, reject)
: resolve(result as TResult1);
} else {
// 值穿透: onFulfilled 不是函数时,用当前值 resolve
resolve(this.value as unknown as TResult1);
}
} catch (e) {
reject(e);
}
};
const handleRejected = () => {
try {
if (typeof onRejected === 'function') {
const result = onRejected(this.reason);
result instanceof MyPromise
? result.then(resolve as any, reject)
: resolve(result as TResult1);
} else {
// 错误穿透: onRejected 不是函数时,继续向下传播错误
reject(this.reason);
}
} catch (e) {
reject(e);
}
};
if (this.state === State.FULFILLED) {
queueMicrotask(handleFulfilled);
} else if (this.state === State.REJECTED) {
queueMicrotask(handleRejected);
} else {
this.onFulfilledCallbacks.push(handleFulfilled);
this.onRejectedCallbacks.push(handleRejected);
}
});
}
catch<TResult = never>(
onRejected?: OnRejected<TResult>
): MyPromise<T | TResult> {
return this.then(undefined, onRejected);
}
finally(onFinally?: (() => void) | undefined | null): MyPromise<T> {
return this.then(
// 规范: 如果 onFinally 返回一个 Promise,等它完成后再继续
value => {
if (typeof onFinally === 'function') {
const result = onFinally();
if (result instanceof MyPromise) {
return result.then(() => value);
}
}
return value;
},
reason => {
if (typeof onFinally === 'function') {
const result = onFinally();
if (result instanceof MyPromise) {
return result.then(() => { throw reason; });
}
}
throw reason;
}
);
}
// --- 静态方法 ---
static resolve<U>(value?: U | PromiseLike<U>): MyPromise<U> {
if (value instanceof MyPromise) return value;
return new MyPromise<U>(resolve => resolve(value as U));
}
static reject<U = never>(reason?: any): MyPromise<U> {
return new MyPromise<U>((_, reject) => reject(reason));
}
static all<T extends readonly unknown[] | []>(
promises: T
): MyPromise<{ -readonly [P in keyof T]: Awaited<T[P]> }> {
return new MyPromise((resolve, reject) => {
if (promises.length === 0) return resolve([] as any);
const results: any[] = new Array(promises.length);
let completed = 0;
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
value => {
results[index] = value;
completed++;
if (completed === promises.length) resolve(results as any);
},
reject // 任一个 reject → 立即 reject
);
});
});
}
static race<T extends readonly unknown[] | []>(
promises: T
): MyPromise<T[number]> {
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
MyPromise.resolve(promise).then(resolve, reject);
});
});
}
static allSettled<T extends readonly unknown[] | []>(
promises: T
): MyPromise<{ -readonly [P in keyof T]: PromiseSettledResult<Awaited<T[P]>> }> {
return new MyPromise(resolve => {
if (promises.length === 0) return resolve([] as any);
const results: any[] = new Array(promises.length);
let completed = 0;
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
value => {
results[index] = { status: 'fulfilled', value };
completed++;
if (completed === promises.length) resolve(results as any);
},
reason => {
results[index] = { status: 'rejected', reason };
completed++;
if (completed === promises.length) resolve(results as any);
}
);
});
});
}
static any<T extends readonly unknown[] | []>(
promises: T
): MyPromise<T[number]> {
return new MyPromise((resolve, reject) => {
if (promises.length === 0) return reject(new AggregateError([], 'All promises were rejected'));
const errors: any[] = new Array(promises.length);
let failed = 0;
promises.forEach((promise, index) => {
MyPromise.resolve(promise).then(
resolve, // 任意一个成功 → resolve
reason => {
errors[index] = reason;
failed++;
if (failed === promises.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
}
);
});
});
}
static withResolvers<T>(): {
promise: MyPromise<T>;
resolve: Resolve<T>;
reject: Reject;
} {
let resolve!: Resolve<T>, reject!: Reject;
const promise = new MyPromise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
}几个实现要点:
- Resolution Procedure 在
resolve中处理:如果resolve接收到一个 thenable,必须等待它 resolve。这处理了resolve(new MyPromise(...))这种情况。 queueMicrotask用于延迟执行:已 resolve/reject 的 Promise 的.then()回调必须异步执行。不能用setTimeout(见上文 microtask vs macrotask)。finally的回调如果返回 Promise 需要等待:规范要求finally(fn)中如果fn()返回一个 Promise,后续的.then()要等该 Promise 完成。all的空数组直接 resolve:Promise.all([])返回[],这是规范规定的边界行为。any的空数组 reject AggregateError:与all相反。
Promise 组合子:选择正确的工具
六个组合子各有明确的适用边界。组合子选择错误时,不只是代码风格问题,也可能直接引入逻辑错误:
| 组合子 | 语义 | 使用场景 | 失败条件 |
|---|---|---|---|
all | 全部成功才成功 | 并行请求,缺一不可 | 任一 reject |
allSettled | 全部完成后报告 | 批量操作,每个独立结果都要 | 永不 reject |
race | 最快的获胜 | 超时控制、竞速 | 最快的 reject 则 reject |
any | 任意一个成功就好 | 多副本请求(CDN fallback) | 全部 reject 才 reject |
withResolvers | 外部控制 resolve | Promise + 回调桥接 | 由使用者控制 |
try(ES2024 提案) | 包装任意函数返回值 | 不确定 fn 是同步还是异步 | fn 抛出则 reject |
race 的一个生产坑:如果快速 reject 的那个 Promise 不是你关心的,race 仍然会因为最快的失败而失败:
js
const data = fetch('/api/data'); // 200ms
const timeout = new Promise((_, reject) =>
setTimeout(() => reject('timeout'), 100) // 100ms → 先 reject
);
await Promise.race([data, timeout]); // → 'timeout',data 请求还在进行中!race 只关心第一个 settled 的结果,但它不会取消慢的那个 Promise。对于超时控制,更好的做法是用 AbortController:
js
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch('/api/data', { signal: controller.signal });
} finally {
clearTimeout(timeout);
}async/await 与原始 Promise 的本质差异
async/await 不是 Promise.then() 的语法糖。两者在三个维度上有本质差异:
1. 执行暂停语义
js
// Promise 链:每次 .then() 后立即继续,不暂停外部代码
function chain() {
fetchData()
.then(a => processA(a))
.then(b => processB(b));
console.log('继续执行'); // ← 立即输出
}
// async/await:await 之后的代码被挂起,外部代码继续
async function asyncFn() {
const a = await fetchData(); // ← 这里暂停 asyncFn 的执行
const b = await processA(a); // ← 但主线程继续
processB(b);
}await 将函数执行"拆分"为多个 microtask:await 之前的代码同步执行,await 之后的代码被注册为 microtask。这与 Generator 函数的 yield 行为类似——事实上 async/await 在规范层面就是 Generator + Promise 的组合。
2. 错误传播
js
// Promise 链:错误沿链向下传播到最近的 .catch()
fetchData()
.then(a => { throw new Error('fail'); })
.then(b => console.log('不会执行'))
.catch(e => console.error('捕获到:', e));
// async/await:错误可以用 try-catch 捕获
async function fn() {
try {
const a = await fetchData();
throw new Error('fail');
const b = await processA(a); // 不会执行
} catch (e) {
console.error('捕获到:', e);
}
}try-catch 可以跨多个 await 统一捕获错误,比 .catch() 链在视觉上更清晰。但要注意:await 表达式后面的代码可能在 try 块中但错误可能来自更早的 await——这恰好是期望行为。
3. 并发表达力
js
// ❌ 串行——两个请求不需要依赖但被串行执行
const a = await fetchA();
const b = await fetchB(); // 等 a 完成后才开始
// ✅ 并行——用 Promise.all 同时发起
const [a, b] = await Promise.all([fetchA(), fetchB()]);
// ✅ 并发控制——限制同时进行的请求数
async function asyncPool(limit, tasks, fn) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const p = Promise.resolve().then(() => fn(task));
results.push(p);
executing.add(p);
const clean = () => executing.delete(p);
p.then(clean, clean);
if (executing.size >= limit) {
await Promise.race(executing); // 等待任意一个完成
}
}
return Promise.all(results);
}async/await 默认是串行的——每个 await 都要等上一个完成。实现并发需要用 Promise.all、Promise.race 或手动管理执行池。这是 async/await 最容易误导初学者的地方:看起来像同步代码,但默认是串行执行。
生产中的 Promise 错误处理
Unhandled Rejection 的检测
Promise 被 reject 但没有 .catch() 或 await 的错误处理时,浏览器触发 unhandledrejection 事件:
js
window.addEventListener('unhandledrejection', event => {
console.error('未处理的 Promise 拒绝:', event.reason);
// 上报到监控系统
analytics.track('unhandled_rejection', {
message: event.reason?.message,
stack: event.reason?.stack,
url: location.href,
});
event.preventDefault(); // 阻止默认的控制台错误输出
});浏览器检测 unhandled rejection 的机制:当一个 Promise 被 reject 时,引擎不立即判断"未处理",而是等待一个 microtask tick。如果在这个 tick 内,有 .catch() 或 then(undefined, onReject) 被注册到这个 Promise,它就不再是 unhandled。过了这个 tick 仍然没有处理,unhandledrejection 事件触发。
这个机制意味着:
js
const p = Promise.reject('error');
// 此时还没有 .catch()
setTimeout(() => {
p.catch(e => console.log('处理了')); // 在 macrotask 中才注册 → 太晚了
}, 0);
// unhandledrejection 事件已经被触发了Node.js 中 --unhandled-rejections=strict 标志会将 unhandled rejection 转为进程退出,在生产环境中通常不会开启。更常见的做法是全局监听 unhandledRejection 事件并上报。
生产中的常见 Promise 陷阱
- 忘记 return Promise:
js
// ❌ 这个 .catch() 捕获不到错误,因为外部没有 return
function fetchWrapper() {
try {
fetchData().catch(e => console.error(e)); // catch 了,但没有 return
} catch (e) {
// 永远不会执行——Promise 的 reject 不会 throw 到同步代码
}
}- forEach 中的 async 不会等待:
js
// ❌ forEach 中的 async 函数不会被等待
[1, 2, 3].forEach(async (id) => {
await processItem(id); // forEach 不关心返回值
});
console.log('done'); // ← 在所有 processItem 完成之前就输出了
// ✅ for...of 会正确等待
for (const id of [1, 2, 3]) {
await processItem(id);
}- Promise 构造函数中的错误被吞掉:
js
// ❌ new Promise 构造函数中的同步错误被自动 reject,但不记录
new Promise((resolve, reject) => {
throw new Error('constructor error'); // → reject,但如果没 .catch() 就是 unhandled
});
// ✅ 构造函数中总是 try-catch 或传入 AbortSignal
new Promise((resolve, reject) => {
try {
doSomething();
} catch (e) {
reject(e);
}
});扩展问题
Q1: Promise.resolve().then(() => {}).then(() => {}) 中有多少个 microtask?它们的调度顺序是怎样的?
两个 microtask。第一个 Promise.resolve() 是已 resolved 状态,但其 .then() 回调不会在当前 tick 的同步代码中执行——规范要求它被调度到 microtask queue。
执行顺序:
- 当前同步代码执行完毕
- Microtask 1(第一个
.then())被执行 - 第一个
.then()返回一个新的 resolved Promise - Microtask 2(第二个
.then())被加入 microtask queue - Microtask 2 执行
关键点:即使 Promise 已经是 resolved 状态,.then() 回调仍然是异步的。这是在 Promise/A+ 规范 §2.2.4 中明确要求的——"onFulfilled or onRejected must not be called until the execution context stack contains only platform code"。
Q2: Promise.all 如果传入的数组中有非 Promise 值,会发生什么?为什么这样设计?
Promise.all 会用 Promise.resolve() 包裹每个元素。非 Promise 值会被转换为立即 resolved 的 Promise。所以 Promise.all([1, 2, Promise.resolve(3)]) 等价于 Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)])。
这样设计的意图是:Promise.all 的调用者不需要先确保数组中所有值都是 Promise。你可以传入混合数组,all 自己处理类型统一。这在"我有一个值列表,其中有些可能是异步的"场景下大幅简化了调用方代码。
Q3: Promise.race 和 Promise.any 在"第一个 resolve 后"的行为有什么不同?
race:第一个 settled(无论 fulfilled 还是 rejected)就返回它的结果。如果最快的是 reject,race 也 reject,即使有其他 Promise 后续成功。
any:第一个 fulfilled 就返回它的值。如果全部 reject,返回 AggregateError。any 忽略 reject,只关心第一个成功。
这个差异决定了使用场景:
race→ 超时控制(timeout 先 reject → 超时生效)any→ 多副本请求(哪个 CDN 先返回成功就用哪个,失败的副本不影响结果)
Q4: 在 async 函数中,await 之后的代码是什么时候执行的?如果 await 的对象是一个已经 resolved 的 Promise,行为有区别吗?
await 等价于 .then()——它总是将之后的代码作为 microtask 调度,即使 await 的对象已经是 resolved 状态。没有"同步优化"。
js
async function test() {
console.log('A');
await Promise.resolve('ok');
console.log('B'); // 总是异步执行
}
test();
console.log('C');
// 输出: A → C → B即使 Promise.resolve('ok') 是已 resolved 的,B 仍然被放到 microtask queue,在当前 macrotask 的同步代码全部执行完之后才输出。
Q5: 手写 Promise 的 then 中,为什么需要判断 result instanceof MyPromise?不能只判断 typeof result === 'object' && typeof result.then === 'function' 吗?
可以,而且规范就是这样要求的(thenable 鸭子类型检测)。用 instanceof MyPromise 是一种优化——对于自己库创建的 Promise,走 instanceof 快速路径;对于其他库的 Promise(thenable),走 .then() 检测路径。
但只用 instanceof 是不够的——如果你的 Promise 和另一个 Promise 库交互(比如你的代码同时用了 Bluebird),Bluebird 的 Promise 不是你的 MyPromise 的实例,但它有 .then() 方法。所以规范的 thenable 检测(typeof x === 'object' && typeof x.then === 'function')是确保跨库互操作性的必要条件。instanceof 只是对自身库的快捷路径。
在完整实现中,两者应该并用:先检查 instanceof(快速自检),不匹配时 fallback 到 thenable 检测(通用互操作)。
