forEach
和 async/await
的这个组合,就像一对貌合神离的“情侣”,看起来般配,实则互相“背叛”。这个坑,我结结实实地踩过,而且不止一次。
故事的开始:一个看似无害的需求
想象一下,接到一个需求:批量更新一组用户的状态。后端提供了一个接口 updateUser(userId)
,它是一个返回 Promise 的异步函数。第一反应可能就是这样写:
const userIds = [1, 2, 3, 4, 5];
async function updateUserStatus(id) {
console.log(`开始更新用户 ${id}...`);
// 模拟一个需要 1 秒的网络请求
await new Promise(resolve => setTimeout(resolve, 1000));
console.log(`✅ 用户 ${id} 更新成功!`);
return { success: true };
}
async function batchUpdateUsers(ids) {
console.log("--- 开始批量更新 ---");
ids.forEach(async (id) => {
await updateUserStatus(id);
});
console.log("--- 所有用户更新完毕!---"); // ⚠️ 问题的根源在这里!
}
batchUpdateUsers(userIds);
运行这段代码,控制台输出了什么?不是期望的按顺序等待,而是这样的结果:
看到了吗?“所有用户更新完毕!”
这句话几乎是立即打印出来的,它根本没有“等待”任何 updateUserStatus
函数的完成。
问题剖析:forEach
到底干了什么?
forEach
被设计为同步迭代器。它的工作很简单:遍历数组中的每个元素,并为每个元素同步地调用你提供的回调函数。它不关心你的回调函数是同步的还是异步的,也不关心它返回什么。
换句话说,forEach
的内心独白是:
“我的任务就是触发,触发,再触发。至于你传进来的那个 async
函数什么时候执行完?抱歉,那不归我管,我不会等它的。”
正确的姿势:如何真正地“等待”?
既然 forEach
不行,那我们该用什么?答案是使用那些“懂” Promise 的循环方式。
方案一:老实人 for...of
循环(顺序执行)
如果我们需要按顺序、一个接一个地执行异步操作,for...of
循环是你的最佳选择。它是 async/await
的天作之合。
async function batchUpdateUsersInOrder(ids) {
console.log("--- 开始批量更新 (顺序执行) ---");
for (const id of ids) {
// 这里的 await 会实实在在地暂停 for 循环的下一次迭代
await updateUserStatus(id);
}
console.log("--- 所有用户更新完毕!(这次是真的) ---");
}
运行结果:
这完全符合我们的直觉:等待上一个完成后,再开始下一个。
方案二:效率先锋 Promise.all
+ map
(并行执行)
在很多场景下,我们并不需要严格地按顺序执行。这些异步任务之间没有依赖关系,完全可以并行处理以提高效率。这时,map
和 Promise.all
的组合就闪亮登场了。
Array.prototype.map
:与 forEach
不同,map
会返回一个新数组。当我们给它一个 async
函数时,它会同步地返回一个由 pending
Promise 组成的数组。Promise.all
:这个方法接收一个 Promise 数组,并返回一个新的 Promise。只有当数组中所有的 Promise 都成功完成(resolved)时,这个新的 Promise 才会完成。
async function batchUpdateUsersInParallel(ids) {
console.log("--- 开始批量更新 (并行执行) ---");
// 1. map 会立即返回一个 Promise 数组
const promises = ids.map(id => updateUserStatus(id));
// 2. Promise.all 会等待所有 promises 完成
await Promise.all(promises);
console.log("--- 所有用户更新完毕!(这次是真的,而且很快) ---");
}
运行结果:
这种方式的总耗时约等于最慢的那个异步任务的耗时,效率极高。
方案三:更灵活的 for...in
和传统 for
循环
for...in
(用于遍历对象键)和传统的 for (let i = 0; ...)
循环同样支持 await
。它们的工作方式与 for...of
类似,都会等待 await
的 Promise 完成。
// 传统 for 循环
for (let i = 0; i < ids.length; i++) {
await updateUserStatus(ids[i]);
}
为了防止你和我一样踩坑,这里有一份速记备忘录:需要按顺序执行使用 for...of
;需要并行执行,提高效率使用 Promise.all
+ map
,性能最佳,但要注意并发数过高可能带来的问题;绝对不要用 forEach
,它不会等待我们的 await
,它只会无情地触发。
阅读原文:https://mp.weixin.qq.com/s/QUynwSg3aBjzsNo5WttXDQ
该文章在 2025/7/1 9:36:26 编辑过