这是一个非常典型的面试题,也是真实业务里绕不开的场景:页面同时发起 5 个请求,但结果必须按 1、2、3、4、5 的顺序展示。
难点在于"并发"和"有序"天生矛盾:并发追求快的先返回,有序要求慢的也得等前面。这篇文章给出三种常见实现,并分析各自的取舍。
一、先明确问题
假设有下面这组异步任务(task() 调用即发起请求,返回 Promise):
const tasks = [
() => delay(300).then(() => '结果 1'),
() => delay(100).then(() => '结果 2'),
() => delay(200).then(() => '结果 3'),
];
function delay(ms) {
return new Promise((resolve) => setTimeout(() => resolve(ms), ms));
}
如果直接 Promise.all(tasks.map((fn) => fn())),结果数组顺序是对的,但输出时机是并发的——谁先返回谁先处理。而题目要求的是:三个请求同时发出,但打印顺序必须是 1、2、3。
所以核心是:并发执行,但按原顺序消费(或输出)结果。
二、方案一:Promise.all 后按顺序输出
最简单的方式:等所有请求都完成后,再按数组顺序逐条输出。
const results = await Promise.all(tasks.map((fn) => fn()));
results.forEach((r) => console.log(r)); // 结果 1、结果 2、结果 3
优点:代码最短,顺序绝对正确。
缺点:必须等最慢的那个返回,才能开始输出,实时性差。如果第一个请求早就回来了,用户却要等第三个请求完成才看到第一条,体验不好。
三、方案二:async/await 逐个等待(串行)
用 for...of 配合 await,在循环里调用任务:
for (const fn of tasks) {
console.log(await fn()); // 逐个等待,顺序输出
}
输出顺序正确,第一个结果一到就能打印,实时性好。但要注意:这是串行执行——第二个请求要等第一个完成后才发起,总耗时是三者之和。只有任务之间真的存在依赖(比如后一个要用前一个的 token)时才适合。
四、方案三:先并发启动,再按序 await(推荐)
如果想要"并发发出 + 按顺序输出 + 实时打印",可以先把所有任务启动,拿到一组 Promise,再在循环里按顺序 await:
// 第一步:map 是同步的,循环里立即调用 task(),请求全部并发发出
const pendingPromises = tasks.map((task) => task());
// 第二步:按原数组顺序逐个 await,输出自然有序
const printInOrder = async (promises) => {
for (const promise of promises) {
console.log(await promise); // 注意是 await promise,不是 await promise()
}
};
printInOrder(pendingPromises);
为什么它能同时满足"并发"和"有序":
tasks.map((task) => task())在同步循环中立即调用每个任务,所有请求同时发出,实现了并发。for...of里的await按数组原顺序逐个等待,所以输出顺序固定。await对已经 resolve 的 Promise 会立即返回,所以第一个结果一就绪就能立刻打印,不必等最慢的那个。
一个小坑:map 之后数组里已经是 Promise,不是函数了,循环里必须写 await promise。如果手滑写成 await promise(),会报 TypeError: promise is not a function。
补一个错误处理的写法
如果后面的任务先 reject,而它还没轮到被 await,会触发 UnhandledPromiseRejection。稳妥做法是先把每个 Promise 兜底成结果对象,再按序 await:
const pendingPromises = tasks.map((task) =>
task().then(
(value) => ({ ok: true, value }),
(error) => ({ ok: false, error })
)
);
for (const promise of pendingPromises) {
const res = await promise;
if (!res.ok) throw res.error;
console.log(res.value);
}
五、三种方案怎么选?
| 方案 | 是否并发 | 是否实时输出 | 适用场景 |
|---|---|---|---|
Promise.all 后按顺序输出 | 是 | 否,要等最慢 | 结果一次性渲染,不在乎实时性 |
| async/await 逐个等待 | 否(串行) | 是 | 任务之间有依赖,必须串行 |
| 先并发启动再按序 await | 是 | 是 | 推荐,并发且保序 |
一句话总结:想要并发又保序,就先把任务 map 成一组 Promise 全部启动,再按原顺序逐个 await。
六、几个容易踩的坑
map之后拿到的是一组 Promise,循环里是await promise,不是await promise()。await写在forEach里不会生效,因为forEach不接受 async 回调的返回值。要顺序执行用for...of。- 用
Promise.all得到的数组顺序是参数顺序,不是返回顺序,但别把"数组有序"和"输出有序"混为一谈——map里如果直接.then(console.log),输出就是乱序的。 - 写
const tasks = tasks.map(...)复用同名变量容易踩 TDZ(暂时性死区)错误,建议换个变量名,比如pendingPromises。