前端深入
从 Promise.all 深入理解 Promise 链和微任务
这篇笔记记录一下我今天重新梳理 Promise 的过程。 起因是在看自己 Next.js 项目首页时,看到了一段代码: 这段代码本身不难理解。 它的意思是并发读取文章、项目、爱好和统计数量,全部完成以后再继续渲染页面。 但我继续想下去以后,发现 Promise 里有很多以前学过但没有真正想透的细节: 今天正好没什么事,

这篇笔记记录一下我今天重新梳理 Promise 的过程。
起因是在看自己 Next.js 项目首页时,看到了一段代码:
const [articles, projects, hobbies, contentCounts] = await Promise.all([
getPublishedArticles(),
getPublishedProjects(),
getPublishedHobbies(),
getPublishedContentCounts(),
]);这段代码本身不难理解。
它的意思是并发读取文章、项目、爱好和统计数量,全部完成以后再继续渲染页面。
但我继续想下去以后,发现 Promise 里有很多以前学过但没有真正想透的细节:
Promise.all 为什么能保持结果顺序?
Promise.resolve(item) 为什么可以同时处理普通值和 Promise?
then 里面到底做了什么?
then 为什么会返回一个新的 Promise?
resolve 一个 Promise 时,为什么外层 Promise 会跟随内层 Promise?
Promise 链执行时,微任务之间会不会插入其他微任务?今天正好没什么事,就顺着这个点把 Promise 的底层流程重新理了一遍。
先说结论
Promise 最核心的东西可以压缩成几句话:
Promise 保存一个异步结果的状态。
then 注册当前 Promise 完成后的 reaction。
then 会返回一个新的 Promise,也就是下一段链式调用要等待的 Promise。
当前 then 回调的返回值,会决定这个新 Promise 的状态。
then 回调不会同步执行,而是通过微任务队列执行。Promise.all 的核心也不神秘:
把传入的每一项都用 Promise.resolve 包一层
给每个 Promise 注册 then
每成功一个就按原始 index 保存结果
成功数量等于总数量时 resolve 总 Promise
任意一个失败时 reject 总 Promise所以 Promise 不是魔法。
它主要是在做三件事:
保存状态
保存回调
用微任务队列按规则执行回调Promise.all 接收什么
Promise.all 最常见的写法是传一个 Promise 数组:
const result = await Promise.all([
fetchUser(),
fetchArticles(),
fetchProjects(),
]);但更准确地说,它接收的是一个 Iterable。
也就是可以被 for...of 遍历的对象。
数组当然可以:
await Promise.all([
Promise.resolve(1),
Promise.resolve(2),
]);Set 也可以:
await Promise.all(
new Set([
Promise.resolve(1),
Promise.resolve(2),
]),
);甚至普通值也可以混进去:
const result = await Promise.all([
Promise.resolve(1),
2,
"hello",
]);
console.log(result);结果是:
[1, 2, "hello"]普通值会被当成已经成功的 Promise。
Promise.all 为什么能保持顺序
Promise.all 并不是谁先完成谁排前面。
它返回结果的顺序永远和传入顺序一致。
例如:
const slow = new Promise((resolve) => {
setTimeout(() => resolve("slow"), 1000);
});
const fast = new Promise((resolve) => {
setTimeout(() => resolve("fast"), 100);
});
const result = await Promise.all([slow, fast]);
console.log(result);结果是:
["slow", "fast"]虽然 fast 先完成,但它传入时在第二个位置,所以结果也在第二个位置。
可以用一个简化版实现理解:
function myPromiseAll<T>(
items: Iterable<T | Promise<T>>,
): Promise<T[]> {
return new Promise((resolve, reject) => {
const arr = Array.from(items);
const results: T[] = [];
let completed = 0;
if (arr.length === 0) {
resolve([]);
return;
}
arr.forEach((item, index) => {
Promise.resolve(item)
.then((value) => {
results[index] = value;
completed += 1;
if (completed === arr.length) {
resolve(results);
}
})
.catch((error) => {
reject(error);
});
});
});
}重点是这一句:
results[index] = value;每个 Promise 完成时,都把结果放回自己原来的位置。
所以结果顺序稳定。
为什么要 Promise.resolve(item)
手写 Promise.all 时会看到这句:
Promise.resolve(item)它的作用是把任何输入统一变成 Promise。
如果 item 是普通值:
Promise.resolve(123);它会变成一个已经 fulfilled 的 Promise。
如果 item 本来就是 Promise:
const p = fetch("/api/user");
Promise.resolve(p);它会跟随这个 Promise。
如果 item 是 thenable,也就是有 .then() 方法的对象:
const thenable = {
then(resolve: (value: string) => void) {
resolve("done");
},
};
Promise.resolve(thenable).then((value) => {
console.log(value);
});它也会被当成 Promise 来处理。
所以:
Promise.resolve(item).then(...)比直接:
item.then(...)稳得多。
因为普通值没有 .then(),直接调会报错。
Promise.resolve 相当于把输入标准化:
普通值 -> 已成功的 Promise
Promise -> 跟随它的状态
thenable -> 转成真正的 Promisethen 是 Promise 最核心的方法
Promise 最关键的接口是 then。
它做的事情大概有三件:
注册当前 Promise 成功或失败后的回调
创建并返回一个新的 Promise
用回调的执行结果决定这个新 Promise 的状态例如:
const p1 = Promise.resolve(1);
const p2 = p1.then((value) => {
return value + 1;
});这里的 p2 就是 then 返回的新 Promise。
我们可以把它叫做 nextPromise。
它代表:
当前 then 回调执行之后的结果。所以链式调用并不是同一个 Promise 一直往后挂。
而是每次 then 都返回一个新的 Promise:
const p1 = Promise.resolve(1);
const p2 = p1.then((value) => value + 1);
const p3 = p2.then((value) => value * 2);
const p4 = p3.then((value) => {
console.log(value);
});关系是:
p1 成功 -> 执行第一个 then -> resolve p2
p2 成功 -> 执行第二个 then -> resolve p3
p3 成功 -> 执行第三个 then -> resolve p4nextPromise 到底是什么
nextPromise 不是规范里的正式名字。
在 ECMAScript 规范里,它更接近:
resultCapability.[[Promise]]可以理解成:
const resultCapability = {
promise: nextPromise,
resolve: nextPromiseResolve,
reject: nextPromiseReject,
};then 内部大概会做:
Promise.prototype.then = function (onFulfilled, onRejected) {
const promise = this;
const resultCapability = NewPromiseCapability();
PerformPromiseThen(
promise,
onFulfilled,
onRejected,
resultCapability,
);
return resultCapability.promise;
};这里返回的:
resultCapability.promise就是 nextPromise。
为什么不只创建 Promise,还要把 resolve 和 reject 一起保存?
因为当前 then 的回调将来执行完以后,需要用它的结果来改变 nextPromise 的状态。
例如:
const p2 = p1.then((value) => {
return value + 1;
});内部执行当前 reaction 时,大概是:
queueMicrotask(() => {
try {
const result = onFulfilled(p1.value);
resultCapability.resolve(result);
} catch (error) {
resultCapability.reject(error);
}
});如果回调返回普通值:
return 2;那么 p2 成功,值是 2。
如果回调抛错:
throw new Error("boom");那么 p2 失败,原因是这个错误。
如果回调返回另一个 Promise:
return fetch("/api/user");那么 p2 会跟随这个 Promise。
这就是 nextPromise 的作用:
它接住当前 then 回调的执行结果,
并把这个结果交给链条里的下一步。then 不会找错回调
我一开始容易疑惑:
resultCapability.resolve(result);它怎么知道这个 result 是哪个 onFulfilled 返回的?
不会把别的 then 搞混吗?
答案是不会。
因为每一次 then 都会创建一条 reaction 记录。
这条 reaction 里面同时保存两样东西:
const reaction = {
handler: onFulfilled,
capability: resultCapability,
};也就是说:
这个 then 的回调
和
这个 then 返回的新 Promise 的 resolve/reject
是一对一绑在一起的。比如:
const p2 = p1.then((value) => value + 1);内部会创建:
const reaction = {
handler: (value: number) => value + 1,
capability: {
promise: p2,
resolve: p2Resolve,
reject: p2Reject,
},
};如果 p1 还是 pending,这条 reaction 就会被存到 p1 上:
p1.fulfillReactions.push(reaction);等 p1 fulfilled 后,才会执行:
for (const reaction of p1.fulfillReactions) {
enqueueMicrotask(() => {
const result = reaction.handler(p1.value);
reaction.capability.resolve(result);
});
}所以不会找错。
因为 handler 和 capability 本来就在同一个 reaction 对象里。
多个 then 挂在同一个 Promise 上
还有一种情况:
const p1 = Promise.resolve(1);
const p2 = p1.then((value) => value + 1);
const p3 = p1.then((value) => value * 10);这里不是 p2 后面接 p3。
而是 p2 和 p3 都挂在 p1 上。
内部关系是:
p1 上的 reaction A:
handler: value => value + 1
capability: p2
p1 上的 reaction B:
handler: value => value * 10
capability: p3当 p1 成功值是 1:
reaction A 执行 -> result = 2 -> resolve p2
reaction B 执行 -> result = 10 -> resolve p3它们互不影响。
这也说明,Promise 链和多个订阅不是一回事。
链式调用是:
p1.then(...).then(...).then(...);多个订阅是:
p1.then(...);
p1.then(...);
p1.then(...);then 回调为什么不是同步执行
看这个例子:
console.log("1");
Promise.resolve("A").then((value) => {
console.log("2", value);
});
console.log("3");输出是:
1
3
2 A原因是:
Promise.resolve("A")会立刻创建一个已经 fulfilled 的 Promise。
但是:
then(...)里面的回调不会立刻执行。
它会进入微任务队列。
所以同步代码先执行完:
console.log("1")
console.log("3")然后才执行微任务:
console.log("2 A")可以记成:
then 这个函数调用是同步的。
then 里面传入的回调是异步微任务。已经成功和还在等待的 Promise 有什么区别
如果 Promise 已经 fulfilled:
const p = Promise.resolve("A");
p.then(() => {
console.log("then");
});调用 then 时,它会立刻把回调排进微任务队列。
如果 Promise 还在 pending:
const p = new Promise((resolve) => {
setTimeout(() => {
resolve("A");
}, 1000);
});
p.then(() => {
console.log("then");
});调用 then 时,回调不会进入微任务队列。
它会先被存在 Promise 里面。
等以后调用:
resolve("A");Promise 状态变成 fulfilled,才会把之前保存的回调排进微任务队列。
所以 then 内部大概有一个判断:
if (promise.state === "fulfilled") {
enqueueMicrotask(fulfillReaction);
} else if (promise.state === "rejected") {
enqueueMicrotask(rejectReaction);
} else {
promise.fulfillReactions.push(fulfillReaction);
promise.rejectReactions.push(rejectReaction);
}这就是我今天比较关键的一个理解点:
已经兑现,就马上排微任务。
还在等待,就先存 reaction。
未来 resolve/reject 时,再排微任务。resolve 一个 Promise 会发生什么
如果这样写:
const inner = Promise.resolve("done");
const outer = new Promise((resolve) => {
resolve(inner);
});
outer.then((value) => {
console.log(value);
});outer 不会成功成 inner 这个对象。
它会跟随 inner 的状态。
所以最后输出的是:
done如果 inner 失败,outer 也会失败。
这叫 Promise 的 resolution procedure。
也就是:
如果 resolve 的值是普通值,当前 Promise 用这个普通值 fulfilled。
如果 resolve 的值是 Promise,当前 Promise 采用那个 Promise 的最终状态。
如果 resolve 的值是 thenable,也会尝试跟随它。所以:
Promise.resolve(Promise.resolve(1))最后不是:
Promise<Promise<number>>而是:
Promise<number>Promise 会自动拆一层。
resolve Promise 时会多等一轮微任务吗
这里有一个比较绕的点。
如果直接 resolve 普通值:
const p = new Promise((resolve) => {
resolve("x");
});
p.then(() => {
console.log("p");
});
Promise.resolve().then(() => {
console.log("microtask");
});通常会输出:
p
microtask但如果 resolve 的是另一个 Promise:
const inner = Promise.resolve("x");
const outer = new Promise((resolve) => {
resolve(inner);
});
outer.then(() => {
console.log("outer");
});
Promise.resolve().then(() => {
console.log("microtask");
});通常会输出:
microtask
outer原因是:
resolve(inner);不是直接让 outer fulfilled。
它需要先进入“采用 inner 状态”的流程。
这个流程本身会经过微任务调度。
所以 outer.then(...) 的回调往往会比直接 resolve 普通值更晚进入微任务队列。
我以前听过一种说法:
resolve 一个 Promise 会等两次事件循环。现在看,这种说法不太精确。
更准确应该是:
它可能多等一轮或几轮微任务,
但不是多等一个完整的宏任务事件循环。Promise 链不是同步递归执行
看这个链:
const p1 = Promise.resolve(1);
const p2 = p1.then((value) => {
return value + 1;
});
const p3 = p2.then((value) => {
return value * 2;
});可以想象成:
p1 上有 reactionA:
handler: value => value + 1
capability: p2
p2 上有 reactionB:
handler: value => value * 2
capability: p3执行时不是:
同步执行 reactionA
同步 resolve p2
同步执行 reactionB
同步 resolve p3而是:
微任务 1:
执行 p1 的 reactionA
result = 2
调用 p2Resolve(2)
p2 状态变 fulfilled
把 p2 上的 reactionB 放进微任务队列
微任务 2:
执行 p2 的 reactionB
result = 4
调用 p3Resolve(4)也就是说:
resolve nextPromise 会推动下一步,
但下一步仍然是通过微任务队列执行。它不是同步递归一路跑到底。
Promise 链中间会夹杂其他微任务吗
会。
因为微任务队列是先进先出。
看这个例子:
const p1 = Promise.resolve(1);
const p2 = p1.then((value) => {
console.log("A");
return value + 1;
});
p2.then((value) => {
console.log("B", value);
});
Promise.resolve().then(() => {
console.log("C");
});输出通常是:
A
C
B 2原因是初始微任务队列大概是:
A reaction
C执行 A reaction 时,p2 被 resolve。
然后 p2 上的 B reaction 被放进微任务队列尾部。
这时队列变成:
C
B reaction所以先执行 C,再执行 B。
这说明:
Promise 链不是一个不可打断的连续同步过程。
每一步 then 都是微任务。
中间可能夹进其他已经排队的微任务。catch 和 finally 也可以理解成 then 的变体
catch 大致等价于:
promise.then(undefined, onRejected);所以:
fetch("/api/user")
.then((response) => response.json())
.catch((error) => {
console.error(error);
});其实也是一条 Promise 链。
finally 则是不关心成功值或失败原因,只是在结束时执行一段逻辑。
它也会返回一个新的 Promise。
所以 Promise 的核心仍然是:
then 创建 nextPromise
reaction 执行后 resolve/reject nextPromise回到项目里的 Promise.all
再回头看项目里的代码:
const [articles, projects, hobbies, contentCounts] = await Promise.all([
getPublishedArticles(),
getPublishedProjects(),
getPublishedHobbies(),
getPublishedContentCounts(),
]);现在就能更清楚地理解它:
getPublishedArticles() 先被调用,返回 Promise
getPublishedProjects() 先被调用,返回 Promise
getPublishedHobbies() 先被调用,返回 Promise
getPublishedContentCounts() 先被调用,返回 Promise
Promise.all 给每一项注册 then
谁先完成都可以
完成后按原 index 存结果
四个都完成后,Promise.all 返回的总 Promise fulfilled
await 拿到结果数组
数组按顺序解构成 articles、projects、hobbies、contentCounts它不是让四个任务排队执行。
而是四个任务都先启动。
Promise.all 负责等待它们全部完成。
如果其中一个抛错,并且没有在内部捕获,Promise.all 返回的总 Promise 就会 reject。
不过我这个项目里的读取函数内部基本都有 try/catch 和 fallback。
所以即使数据库暂时不可用,函数也会返回默认内容,不容易让首页直接崩掉。
我最后想通的地方
今天这次学习最大的收获,不是记住了几个 API。
而是把 Promise 链条背后的关系理顺了。
以前我看:
promise
.then(step1)
.then(step2)
.then(step3);会下意识觉得这是一个 Promise 后面接了很多函数。
现在更准确的理解是:
每个 then 都创建一个新的 Promise。
每个 then 都创建一条 reaction。
reaction 里保存当前回调和 nextPromise 的 resolve/reject。
当前 Promise 完成后,执行自己的 reactions。
回调结果会 resolve/reject nextPromise。
nextPromise 完成后,再触发它自己的 reactions。也就是:
p1 -> reactionA -> p2 -> reactionB -> p3 -> reactionC -> p4而不是:
一个 Promise 上同步执行一串函数。另一个想通的点是微任务。
then 回调不会同步执行。
已经 fulfilled 的 Promise,调用 then 时会把回调排进微任务队列。
pending 的 Promise,调用 then 时会先保存 reaction,等以后 resolve/reject 再排微任务。
Promise 链的每一步也是通过微任务往后推进的,所以中间可能夹杂其他已经排队的微任务。
这样再看 Promise.all,就清楚很多:
它不是并发魔法。
它只是统一包装每一项,
给每一项注册 then,
按 index 收集结果,
最后 resolve 自己返回的那个总 Promise。这次是从项目里一行 Promise.all 顺藤摸瓜学下去。
感觉这种学习方式挺舒服的。
不是为了背 API 而学,而是在真实代码里遇到一个点,然后往底层多挖一点。
等下次再看到 Promise 链、await、Promise.all、Promise.resolve 的时候,脑子里就不只是“它能用”,而是能大概知道它内部是怎么一步一步推进的。