Skip to main content

JS await 进阶并发面试题

· 4 min read

JS 并发面试, 道题筛掉九成候选人。

  1. awaitfor 循环里是 串行,10 个请求 10 秒,不是 1 秒
  2. forEach 里写 await 完全无效,外层根本不会等
  3. Promise.all 一个 reject 全盘崩,要容错必须用 Promise.allSettled

正解:并发用 Promise.all(arr.map(fn)),需要限流用 p-limit,要容错换 allSettled

关键提醒:await 只暂停当前 async 函数,不会暂停外层调用者,更不会暂停 forEach 的迭代。


第一题:for 循环里的 await 到底卡在哪

async function fetchAll(urls) {
const results = [];
for (const url of urls) {
results.push(await fetch(url));
}
return results;
}

面试官问:10 个 URL,每个请求 1 秒,总耗时多少?

答 10 秒的过线,答 1 秒的回家。await 会让 for 循环停在那一行,等 Promise resolve 才进入下一次迭代。10 个请求老老实实排队,CPU 在那干等。

想并发就一行改动:

async function fetchAll(urls) {
return Promise.all(urls.map((url) => fetch(url)));
}

map 一次性把 10 个 fetch 全发出去,Promise.all 等所有 resolve。总耗时 ≈ 最慢那个请求的时间。

我之前以为这是常识,结果在 review 里看到太多 for...of + await 写法,问写的人为啥,回答是「await 不就是并发吗」。await 是同步化的语法糖,本质是串行,别搞反了。

第二题:forEach 里的 await 为啥不生效

async function badExample(urls) {
urls.forEach(async (url) => {
await fetch(url);
});
console.log('done'); // 这行会立刻打印
}

'done' 在所有 fetch 发出去之前就打印了。原因很简单:forEach 不认 Promise,它只是同步遍历调用回调,回调返回的 Promise 被直接丢掉。

forEach 的类型签名是 (callback) => void,连返回值都没有。你传 async 函数进去,它收下回调返回的 Promise,转头扔进垃圾桶。

要么换 for...of(串行),要么换 Promise.all + map(并发):

for (const url of urls) await fetch(url); // 串行
await Promise.all(urls.map((url) => fetch(url))); // 并发

记住一条规则:只有 for / for...of / for...in / while 这类原生循环能配合 await,所有数组方法都不行mapfilterforEachreduce 全部不行)。

第三题:Promise.all 的容错陷阱

const results = await Promise.all([
fetch('/api/a'),
fetch('/api/b'), // 这个挂了
fetch('/api/c'),
]);

/api/b reject,整个 Promise.all 立刻 reject,ac 的结果你拿不到——哪怕它们已经成功了。

线上场景里这很要命。比如首页加载 5 个模块的数据,其中推荐位的接口偶尔抖一下,整页就白屏。

正确做法是 Promise.allSettled

const results = await Promise.allSettled([
fetch('/api/a'),
fetch('/api/b'),
fetch('/api/c'),
]);

results.forEach((r) => {
if (r.status === 'fulfilled') console.log(r.value);
else console.error(r.reason);
});

每个结果都有 status 字段,要么 fulfilledvalue,要么 rejectedreason。不会因为一个失败就拖累全部。

进阶:并发限流

Promise.all(urls.map(fetch)),URL 有 1000 个怎么办?浏览器同域名连接数有限(一般 6),服务端也扛不住。

手撸一个限流不难:

async function pLimit(tasks, limit) {
const results = [];
const executing = [];
for (const task of tasks) {
const p = task().then((r) => {
executing.splice(executing.indexOf(p), 1);
return r;
});
results.push(p);
executing.push(p);
if (executing.length >= limit) await Promise.race(executing);
}
return Promise.all(results);
}

await pLimit(urls.map((u) => () => fetch(u)), 6);

核心思路:Promise.race 等任意一个完成就放下一个进来,始终保持 limit 个并发。生产环境直接用 p-limit 库就行,没必要自己造。

一句话总结这四个 API

API行为失败处理
for + await串行抛错中断
Promise.all全并发任一失败全失败
Promise.allSettled全并发各自独立,全部等完
Promise.race全并发第一个完成(无论成败)即返回

面试时被问到,直接画这张表,比背概念清楚一百倍。