JavaScript 的 forEach 加了 async,为什么 await 仍没等完?

 1天前     19  

文章目录

批量处理写成 await list.forEach(async ...),看起来已经“等待循环”,后面的代码却先跑了。问题不在于少写了一个 async,而在于:forEach 根本没有把每个回调的 Promise 交回来。[3]

这次在 Node.js v22.23.1 里用本地延时任务做了小实验,没有发送网络请求,也没有执行真实保存。它只验证等待和结果收集,不证明任何线上接口的速度或成功率。

外面的 await,等不到里面的任务

下面是一段可以直接运行的完整 JavaScript:

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

async function main() {
  const done = [];
  await [1, 2].forEach(async id => {
    await sleep(10);
    done.push(id);
  });
  console.log(done); // [],不是 [1, 2]
}

main().catch(console.error);

实验里,紧跟循环的读取确实得到空数组;等回调后来完成,数组才变成 [1, 2]。所以空数组并不代表这些任务被取消,只代表读取时还没等到它们。

MDN 明确说明 forEach 不等待 Promise,并且返回 undefined。回调返回了什么,不会变成 forEach 的返回结果。外面再加 await,也不会自动收集被丢弃的 Promise。[3]

需要按顺序完成:用 for...of

如果后一项依赖前一项,就让等待发生在真正的循环里。下面的 processItem 代表你已有的异步处理函数:

async function runInOrder(items, processItem) {
  const results = [];
  for (const item of items) {
    results.push(await processItem(item));
  }
  return results;
}

实验中用相同类型的延时任务逐项处理,函数返回时,两项结果已经收齐。这是顺序执行的选择,不是“所有异步操作都应该串行”的规则。

任务彼此独立:用 map 收集,再 Promise.all

async function runTogether(items, processItem) {
  const tasks = items.map(item => processItem(item));
  return await Promise.all(tasks);
}

这里 map 把每项返回的 Promise 放进数组,Promise.all 才有一组明确的等待对象。全部成功时,结果按输入顺序排列,不按完成先后排列。[4]

本实验故意让第 2 项先完成:实际完成顺序是 [2, 1],收集到的结果仍为 [10, 20]。不要把结果数组的顺序,当成任务执行顺序的证据。

“一起等”不等于“全成功或全撤销”

Promise.all 在其中一个 Promise 拒绝时就会拒绝,并不是数据库事务。[4]另一个受控实验中,一项先失败,另一项仍在等待;捕获失败后放行后一项,它照样完成了。这只能证明本例没有自动取消,不能据此推断所有业务操作的可撤销性。

因此,批量真实写入还要单独设计失败处理。数据量大时,也不要把所有任务一次性启动:上面的简例没有并发限流,适合少量、可同时开展的独立任务,不是大批量请求方案。

我的选择标准很简单:同步副作用可以用 forEach;顺序异步用 for...of;少量独立异步用 map 加 Promise.all。先说清“什么时候算这一批完成”,再选循环写法,别靠增加 await 的数量碰运气。

资料核验:2026年10月21日。机制依据来自 MDN;结果来自 Node.js 隔离实验,不是生产事故复盘。本文由 AI 辅助整理。

参考资料

  1. MDN:Array.prototype.forEach()
  2. MDN:Promise.all()
版权声明:leif 发表于 1天前,共 1581 字。
转载请注明:JavaScript 的 forEach 加了 async,为什么 await 仍没等完? | 虎娃博客

JavaScript 的 forEach 加了 async,为什么 await 仍没等完?已关闭评论

评论已关闭...
暂无评论...