你在 forEach 里写的 await,其实根本没在等!


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 的组合就闪亮登场了。

  1. **Array.prototype.map ** :与 forEach 不同, map 会返回一个新数组。当我们给它一个 async 函数时,它会同步地返回一个由 pending Promise 组成的数组。
  2. **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 ,它只会无情地触发。