JavaScript forEach 不支持 await:for...of 顺序执行与 Promise.all 并发执行

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(并发)。