JS await 进阶并发面试题
JS 并发面试,三 道题筛掉九成候选人。
await在for循环里是 串行,10 个请求 10 秒,不是 1 秒forEach里写await完全无效,外层根本不会等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,所有数组方法都不行(map、filter、forEach、reduce 全部不行)。
第三题:Promise.all 的容错陷阱
const results = await Promise.all([
fetch('/api/a'),
fetch('/api/b'), // 这个挂了
fetch('/api/c'),
]);
/api/b reject,整个 Promise.all 立刻 reject,a 和 c 的结果你拿不到——哪怕它们已经成功了。
线上场景里这很要命。比如首页加载 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 字段,要么 fulfilled 带 value,要么 rejected 带 reason。不会因为一个失败就拖累全部。
进阶:并发限流
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 | 全并发 | 第一个完成(无论成败)即返回 |
面试时被问到,直接画这张表,比背概念清楚一百倍。