为什么 forEach 里的 await 没用:JS 异步循环踩坑记

前几天写脚本,想在数组遍历里挨个 await 一下:

const arr = [1, 2, 3];

arr.forEach(async (item) => {
    await sleep(1000);
    console.log(item);
});

console.log('结束');

预期看到 1 2 3 结束,结果输出是:

结束
1
2
3

forEach 完全没等异步回调结束,console.log('结束') 就先跑了。

为什么 forEach 不等待

Array.prototype.forEach 的内部实现,本质就是:

for (let i = 0; i < arr.length; i++) {
    callback(arr[i]);
}

不关心 callback 返回的是不是 Promise,也不收集 Promise,更不会 await。所以哪怕你写:

await arr.forEach(async x => await fn(x));

等价于:

await undefined

因为 forEach 的返回值就是 undefined

正确写法

需要顺序 await:用 for…of

一个执行完再执行下一个,最稳、最好读:

for (const item of arr) {
    await doSomething(item);
}

适合请求限流、数据库写入、有依赖关系的自动化脚本。

需要并发 await:用 Promise.all + map

全部同时开跑,最快:

await Promise.all(
    arr.map(async (item) => {
        await doSomething(item);
    })
);

注意别把上游接口打爆——需要限流的话可以配合 p-limit 这类库。

记忆表

场景写法
顺序 awaitfor...of
并发 awaitPromise.all + map
不需要等待forEach 也无所谓

一条经验

只要方法里出现 await,就别用 forEach

想清楚这一步之后,很多”数据处理完了但结果不对”的 bug 就会消失。