我的JavaScript代码为啥在forEach里没按预期执行?

7 阅读1分钟

上周四凌晨,我盯着监控面板上飙升的错误率,发现一个诡异的规律:所有报错都来自一段看似无害的forEach循环代码。数据量从测试环境的100条变成生产环境的10万条后,这段代码突然开始“发疯”——要么漏处理数据,要么重复处理。你猜怎么着?问题出在forEach的“老实人”性格上。

1. 当forEach遇到异步:一场无声的背叛

场景:我们需要批量调用第三方API更新用户状态,代码长这样:

users.forEach(async (user) => {
  await updateUserStatus(user.id); // 假设这是一个耗时1秒的API调用
});
console.log('全部完成'); // 猜猜这行什么时候执行?
  • 现象*:console.log在第一个await执行后立即打印,而非等所有用户处理完毕。
  • 根因*:forEach的设计根本不在乎你回调里的await。它的执行逻辑是:
  1. 同步遍历数组,对每个元素立即执行回调(不等待回调内的异步操作);
  2. 所有回调被“甩”到事件循环队列后,forEach就认为自己的任务结束了。

本质上,forEach是“一锤子买卖”,它的回调就像一群被同时点燃的爆竹——你无法知道谁先响完。

2. 性能灾难:当10万条数据遇上forEach

我们曾天真地认为“反正都是异步并行,速度差不多”,直到用console.time实测:

方法10万条数据耗时内存峰值
forEach + async6秒1.2GB
for...of + await65秒300MB
Promise.all5.8秒1.5GB
  • 关键发现*:
  • forEach确实“快”,但这是以同时发起10万个并行请求为代价的——第三方API直接返回429(太多请求);
  • 并行 != 并发:forEach粗暴的并行会压垮下游系统,而for...of的串行虽然慢,但稳定。

3. 血泪换来的三种正确写法

解法1:老老实实用for...of(适合需要严格串行的场景)

for (const user of users) {
  await updateUserStatus(user.id); // 逐个处理
}
  • 适用场景*:需要严格保证顺序(如流水线操作)、下游服务扛不住并发冲击时。

解法2:Promise.all控制并发量(性能和稳定的平衡点)

const BATCH_SIZE = 100; // 每次并发100个
for (let i = 0; i < users.length; i += BATCH_SIZE) {
  const batch = users.slice(i, i + BATCH_SIZE);
  await Promise.all(batch.map(user => updateUserStatus(user.id)));
}
  • *为什么不是直接Promise.all(users.map(...))**?——除非你想重现我的生产事故。

解法3:利用p-map等库实现高级控制

import pMap from 'p-map';
await pMap(users, updateUserStatus, { concurrency: 100 }); // 精确控制并发数
  • 推荐库*:p-map、async-pool。这些库帮你处理了重试、错误处理等脏活。

4. 避坑清单:forEach的七个致命陷阱

  1. 无视异步:回调内的await不会暂停遍历(与map/filter等数组方法行为一致);
  2. 无法中断:即使回调内throw error,其他回调依然会继续执行(相比之下for...of可用break提前退出);
  3. 性能幻觉:你以为的“并行优化”可能变成DDOS攻击;
  4. 副作用传染:在回调内修改外部变量时,可能因执行顺序不确定导致竞态条件;
  5. 无返回值:与map不同,forEach永远返回undefined,无法链式调用;
  6. 稀疏数组陷阱:对[1,,3]这样的数组,空元素会直接被跳过(但for...of会遍历到undefined);
  7. 猴子补丁风险:如果有人在Array.prototype.forEach上动了手脚…(没错,我见过有人这么干)。

5. 什么时候该用forEach?

只有一种情况我会用它:同步、无副作用的纯遍历。比如:

// 打印日志、渲染UI等不需要等待的操作
logs.forEach(log => console.log(log)); 
  • 最后送大家一句话*:在JavaScript的异步世界里,forEach是个好员工,但它只会埋头干活,从不抬头看路——你需要明确告诉它“该怎么走”。

你在项目里是怎么处理批量异步任务的?遇到过forEach的其他坑吗?欢迎评论区聊聊你的实战经历。