Array.prototype.forEach 不会等待异步回调完成,这是 JavaScript 里最常见的异步陷阱之一。
问题演示
const arr = [1, 2, 3];
arr.forEach(async (item) => {
await sleep(1000);
console.log(item);
});
console.log('结束');
输出顺序是:
结束
1
2
3
forEach 本身是同步的,不关心回调返回的是否是 Promise,调用后立即继续执行。
await arr.forEach(...) 也没有意义,因为 forEach 的返回值始终是 undefined:
await undefined // 无效等待
顺序执行:用 for…of
for (const item of arr) {
await doSomething(item);
}
特点:一个完成后再执行下一个,顺序稳定。适合:
- 限流的 API 请求(防止并发过多)
- 数据库写入(避免竞争)
- 依赖上一步结果的操作
并发执行:用 Promise.all + map
await Promise.all(
arr.map(async (item) => {
await doSomething(item);
})
);
特点:全部同时启动,等所有完成。适合独立任务批量执行,注意不要把接口打爆。
限制并发数量(每次最多 N 个):
async function runConcurrent(arr, fn, concurrency = 5) {
const results = [];
for (let i = 0; i < arr.length; i += concurrency) {
const batch = arr.slice(i, i + concurrency);
results.push(...await Promise.all(batch.map(fn)));
}
return results;
}
选择参考
| 场景 | 推荐写法 |
|---|---|
| 顺序 await,依赖上一步 | for...of |
| 全部并发,互不依赖 | Promise.all + map |
| 不需要等待结果 | forEach(此时没问题) |
requestIdleCallback:避免长循环阻塞主线程
如果是大量数据的 CPU 密集计算(如点云、坐标变换),不要在主线程单次执行,而是用 requestIdleCallback 分片处理:
function processChunked(data, chunkSize = 1000) {
return new Promise((resolve) => {
let i = 0;
const results = [];
function step(deadline) {
while (i < data.length && deadline.timeRemaining() > 0) {
results.push(expensiveOp(data[i]));
i++;
}
if (i < data.length) {
requestIdleCallback(step);
} else {
resolve(results);
}
}
requestIdleCallback(step);
});
}
特点:浏览器空闲时处理,不阻塞动画和用户交互。Safari 不支持 requestIdleCallback,可以用 setTimeout(fn, 0) 作为降级方案。
一句话总结
涉及 await 就别用 forEach,用 for...of(顺序)或 Promise.all(并发)。
