Skip to content
Promise 的微任务调度与实现
概述
Promise 对象的回调执行顺序由事件循环中的队列机制决定,而不只是简单的异步调用。.then()、.catch()、.finally() 以及 await 之后的代码属于微任务,而 setTimeout、setInterval 等回调属于宏任务。理解这两类队列的清空规则,是解释 Promise 调度行为的基础。
工作原理:微任务队列与事件循环
JavaScript 引擎的事件循环维护两类任务队列:
text
宏任务队列(Task Queue / Macrotask Queue)
├── setTimeout / setInterval
├── I/O 操作
├── UI 渲染
└── postMessage / MessageChannel
微任务队列(Microtask Queue)
├── Promise.then / .catch / .finally
├── queueMicrotask
├── MutationObserver
└── async/await(await 之后的代码等价于 .then() 回调)关键规则:每一个宏任务执行完毕之后,在进入下一个宏任务之前,必须清空当前的微任务队列。 这一规则直接决定了以下代码的输出顺序:
js
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出顺序:1 → 4 → 3 → 2步骤说明:
console.log('1')和console.log('4')是同步代码,在当前宏任务中立即执行。- 当前宏任务结束,开始清空微任务队列。
Promise.resolve().then(…)的回调进入微任务队列,执行console.log('3')。- 微任务队列清空,进入下一个宏任务。
setTimeout回调执行,输出'2'。
queueMicrotask 正是 Promise 内部用来调度 .then() 回调的底层机制。在 V8 引擎中,Promise.prototype.then 的 C++ 实现会调用 EnqueueMicrotask,把回调注册到当前执行上下文的微任务队列中。如果使用 setTimeout(fn, 0) 来模拟异步,回调会被放入宏任务队列,排在已有的所有宏任务之后,并且在两个宏任务之间浏览器可能插入渲染帧,导致回调延迟不可控。微任务则是在当前宏任务结束之后立即执行,并且一次性清空整个队列,中间不会插入渲染。
“一次性清空”的行为也带来了潜在风险:如果在 .then() 回调中递归创建新的 Promise 并链式调用,微任务队列会持续增长,使得主线程一直被微任务占据,浏览器无法插入渲染帧,页面会持续无响应。
js
// 微任务风暴示例:浏览器无法渲染
function microtaskStorm() {
Promise.resolve().then(() => {
doSomeWork();
microtaskStorm(); // 递归注册微任务,队列不会被清空
});
}规范细节:Promise/A+ 的几个关键边界
实现 Promise 时如果不参考规范,很容易出现“看起来正确但不完全符合标准”的行为。Promise/A+ 规范里几个易错点尤其值得注意。
Promise Resolution Procedure(§2.3)
当 .then() 的 onFulfilled 返回一个值 x 时,规范定义了 [[Resolve]](promise, x) 过程:
- 如果
x与promise是同一个对象,以TypeError拒绝(防止循环引用)。 - 如果
x是一个 Promise,则采用x的状态和值。 - 如果
x是对象或函数(不为null),且具有可调用的then方法,则将其视为 thenable,调用x.then(resolve, reject)。 - 否则,直接用
x来 resolvepromise。
thenable 处理是实现中最容易遗漏的部分,但它保证了不同 Promise 库之间的互操作性——任意符合 thenable 接口的对象都可以被 Promise 机制消费。
值的穿透
当 .then() 的 onFulfilled 不是函数时,规范要求沿链向下传递值:
js
Promise.resolve(42)
.then(null) // onFulfilled 非函数 → 值穿透
.then(undefined) // 同上
.then(v => console.log(v)); // 输出 42这不是简单地跳过 null,而是规范明确规定的行为:如果 onFulfilled 不是函数,则用当前值来 resolve 新 Promise。
错误穿透
类似地,当 .then() 的 onRejected 不是函数时,错误沿链向下传播:
js
Promise.reject('error')
.then(v => v) // onRejected 非函数 → 错误穿透
.then(v => v)
.catch(e => console.log(e)); // 输出 'error'thenable 的鸭子类型检测
规范通过检查对象是否拥有可调用的 .then 方法来判断 Promise-like,而不是使用 instanceof Promise。这意味着任何带 .then() 方法的对象都可以被 Promise.resolve() 或 .then() 返回值的处理流程消费:
js
const thenable = {
then(resolve, reject) {
setTimeout(() => resolve('ok'), 1000);
}
};
Promise.resolve(thenable).then(v => console.log(v)); // 约 1 秒后输出 'ok'这一设计刻意保持了与已有 Promise 库(如 Bluebird、Q、jQuery Deferred)的兼容。代价是某些恰好拥有 .then 属性的普通对象可能被误识别为 thenable,不过实际代码中几乎不会遇到这种冲突。
手写 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;
// 循环引用检测(Promise/A+ §2.3.1)
if (value === this) {
reject(new TypeError('Chaining cycle detected for promise'));
return;
}
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;
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);
if (result instanceof MyPromise) {
result.then(resolve as any, reject);
} else if (result !== null && typeof result === 'object' && typeof (result as any).then === 'function') {
(result as PromiseLike<TResult1>).then(resolve, reject);
} else {
resolve(result as TResult1);
}
} else {
// 值穿透
resolve(this.value as unknown as TResult1);
}
} catch (e) {
reject(e);
}
};
const handleRejected = () => {
try {
if (typeof onRejected === 'function') {
const result = onRejected(this.reason);
if (result instanceof MyPromise) {
result.then(resolve as any, reject);
} else if (result !== null && typeof result === 'object' && typeof (result as any).then === 'function') {
(result as PromiseLike<TResult1>).then(resolve, reject);
} else {
resolve(result as TResult1);
}
} else {
// 错误穿透
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(
value => {
if (typeof onFinally === 'function') {
const result = onFinally();
if (result != null && typeof (result as any).then === 'function') {
return (result as PromiseLike<void>).then(() => value);
}
}
return value;
},
reason => {
if (typeof onFinally === 'function') {
const result = onFinally();
if (result != null && typeof (result as any).then === 'function') {
return (result as PromiseLike<void>).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
);
});
});
}
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,
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 };
}
}几个实现要点:
- 循环引用检测:
resolve中提前检查value === this,符合 Promise/A+ §2.3.1,防止resolve(promise)导致死锁。 - Resolution Procedure:
resolve在收到 thenable 时会等待其完成,这处理了resolve(new MyPromise(...))的场景。 queueMicrotask调度:已经 resolve/reject 的 Promise 在执行.then()回调时,总是通过微任务异步调用,而非同步触发。then回调中的 thenable 退化处理:handleFulfilled和handleRejected中先通过instanceof MyPromise检查自产 Promise,不匹配时退化到 thenable 鸭子类型检测,确保非本库的 thenable 也能被正确消费。finally的 thenable 处理:当onFinally返回一个 thenable 对象时(不仅仅是MyPromise实例),通过typeof (result as any).then === 'function'进行鸭子类型检测,并等待该 thenable 完成后再继续,与构造函数及then中的处理逻辑保持一致。all空数组直接 resolve:Promise.all([])返回[],规范明确要求此行为。any空数组 rejectAggregateError:与all相反,全部失败时才 reject。
静态方法:组合子及选用场景
六个组合子各自对应不同的并发语义,选择错误会导致逻辑问题。
| 组合子 | 语义 | 典型场景 | 失败条件 |
|---|---|---|---|
all | 全部成功才成功 | 并行请求,缺一不可 | 任一 reject |
allSettled | 全部完成后报告 | 批量操作,每个独立结果都需要 | 永不 reject |
race | 最快的那个 settled 决定整体结果 | 超时控制、竞速 | 最快的 reject 则整体 reject |
any | 任意一个成功即可 | 多副本请求(例如 CDN 回退) | 全部 reject 时才 reject |
withResolvers | 外部控制 resolve/reject | Promise 与回调风格的桥接 | 由使用者控制 |
try(ES2024 提案) | 包装任意函数,返回 Promise | 不确定函数是同步还是异步时统一处理 | 函数抛出则 reject |
race 的一个常见陷阱:如果最快 reject 的 Promise 并不是你关心的操作,race 仍然会因其失败而失败,且不会取消其他未完成的 Promise。
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 请求仍在进行中对于超时控制,更稳妥的做法是配合 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() 的简单语法糖,两者在三个方面存在本质区别。
执行暂停语义
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 之后的代码总是被注册为微任务,这相当于将函数拆分成多个 .then() 回调。这种机制源自 Generator 函数的 yield——在规范层面,async/await 就是 Generator + Promise 的组合。
错误传播
js
// Promise 链:错误沿链向下传播到最近的 .catch()
fetchData()
.then(a => { throw new Error('fail'); })
.then(b => console.log('不会执行'))
.catch(e => console.error('捕获到:', e));
// async/await:可以跨多个 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 之后的代码都在 try 块内,而错误可能来自更早的 await,这正是期望的行为。
并发表达力
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 完成。这容易使人误以为异步操作会自动并行。实现并发仍然需要显式使用 Promise.all、Promise.race 等组合子。
错误处理
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(); // 阻止控制台默认错误输出
});引擎检测未处理拒绝的时机:当一个 Promise 被 reject 时,引擎不会立即判定为"未处理",而是等待一个微任务滴答(microtask tick)。如果在这一个滴答内,该 Promise 通过 .catch() 或 then(undefined, onReject) 注册了拒绝处理函数,那么它就不再是未处理的。反之,超过一个滴答后仍无处理,则触发 unhandledrejection 事件。
这就意味着:
js
const p = Promise.reject('error');
// 此时还没有 .catch()
setTimeout(() => {
p.catch(e => console.log('处理了')); // 宏任务中才注册 → 太晚了
}, 0);
// unhandledrejection 事件已被触发在 Node.js 中,--unhandled-rejections=strict 标志可将未处理的拒绝转为进程退出。更常见的做法是全局监听 unhandledRejection 事件并上报。
常见陷阱
- 忘记 return Promise:
js
// 外部无法捕获错误
function fetchWrapper() {
try {
fetchData().catch(e => console.error(e)); // catch 了但没有 return
} catch (e) {
// 永远不会执行——Promise reject 不会抛给同步代码
}
}forEach中的async函数不会被等待:
js
// forEach 的回调被忽略返回值
[1, 2, 3].forEach(async (id) => {
await processItem(id);
});
console.log('done'); // 在所有 processItem 完成前就输出了
// for...of 会正确等待
for (const id of [1, 2, 3]) {
await processItem(id);
}- Promise 构造函数中的错误被吞掉:
js
// 构造函数内同步 throw 会被自动 reject,但如果没有 .catch() 就是 unhandled
new Promise((resolve, reject) => {
throw new Error('constructor error');
});
// 显式 try-catch 来控制
new Promise((resolve, reject) => {
try {
doSomething();
} catch (e) {
reject(e);
}
});常见问题
Q1:Promise.resolve().then(() => {}).then(() => {}) 中有多少个微任务?调度顺序是怎样的?
两个微任务。Promise.resolve() 返回已 resolved 的 Promise,但其 .then() 回调仍然按照规范异步调度。执行顺序:
- 当前同步代码执行完毕。
- 第一个微任务(第一个
.then())执行。 - 它返回一个新的 resolved Promise。
- 第二个微任务(第二个
.then())被加入队列,并在当前微任务清空阶段执行。
关键点:即使 Promise 已经是 resolved 状态,.then() 回调仍然是异步的(Promise/A+ §2.2.4)。
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)])。这样设计让调用⽅无需预先统一数组元素的类型。
Q3:Promise.race 与 Promise.any 在第一个 settled 后行为的区别是什么?
race:第一个 settled(无论 fulfilled 还是 rejected)就返回它的结果。如果最快的是 reject,race也 reject。any:第一个 fulfilled 就返回它的值;只有当全部 reject 时才拒绝并返回AggregateError。any忽略拒绝,只关心第一个成功。
Q4:在 async 函数中,await 之后的代码什么时候执行?如果 await 的对象已经是 resolved 的 Promise,有区别吗?
await 总是将后续代码作为微任务调度,即使 await 的对象已经是 resolved 状态,也不会进行同步执行优化。因此:
js
async function test() {
console.log('A');
await Promise.resolve('ok');
console.log('B');
}
test();
console.log('C');
// 输出: A → C → BQ5:手写 Promise 的 then 中为什么需要判断 result instanceof MyPromise,只用 thenable 检测不够吗?
规范要求的 thenable 鸭子类型检测(typeof x === 'object' && typeof x.then === 'function')是互操作性的保障,它兼容任何符合 thenable 接口的对象。instanceof MyPromise 则是针对自身库创建的 Promise 的快速路径。实际实现时应同时使用两种检查:先通过 instanceof 快速判断自产 Promise,不匹配时退化到 thenable 检测。同样地,finally 方法中对 onFinally 返回值的处理也应遵循这一逻辑——先判断是否为 thenable 接口(包括 MyPromise 实例和其他 thenable 对象),再决定是否需要等待其完成。
参考链接
- Promise/A+ 规范:https://promisesaplus.com/
- ECMAScript 2024 规范(Promise 章节):https://tc39.es/ecma262/#sec-promise-objects
- MDN 文档:Promise:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
