上周四凌晨,我盯着监控面板上飙升的错误率,发现一个诡异的规律:所有报错都来自一段看似无害的forEach循环代码。数据量从测试环境的100条变成生产环境的10万条后,这段代码突然开始"发疯"------要么漏处理数据,要么重复处理。你猜怎么着?问题出在forEach的"老实人"性格上。
1. 当forEach遇到异步:一场无声的背叛
场景:我们需要批量调用第三方API更新用户状态,代码长这样:
javascript
users.forEach(async (user) => {
await updateUserStatus(user.id); // 假设这是一个耗时1秒的API调用
});
console.log('全部完成'); // 猜猜这行什么时候执行?
- 现象 *:
console.log在第一个await执行后立即打印,而非等所有用户处理完毕。 - 根因 *:
forEach的设计根本不在乎你回调里的await。它的执行逻辑是:
- 同步遍历数组,对每个元素立即执行回调(不等待回调内的异步操作);
- 所有回调被"甩"到事件循环队列后,
forEach就认为自己的任务结束了。
本质上,forEach是"一锤子买卖",它的回调就像一群被同时点燃的爆竹------你无法知道谁先响完。
2. 性能灾难:当10万条数据遇上forEach
我们曾天真地认为"反正都是异步并行,速度差不多",直到用console.time实测:
| 方法 | 10万条数据耗时 | 内存峰值 |
|---|---|---|
forEach + async |
6秒 | 1.2GB |
for...of + await |
65秒 | 300MB |
Promise.all |
5.8秒 | 1.5GB |
- 关键发现*:
forEach确实"快",但这是以同时发起10万个并行请求为代价的------第三方API直接返回429(太多请求);- 并行 != 并发 :
forEach粗暴的并行会压垮下游系统,而for...of的串行虽然慢,但稳定。
3. 血泪换来的三种正确写法
解法1:老老实实用for...of(适合需要严格串行的场景)
javascript
for (const user of users) {
await updateUserStatus(user.id); // 逐个处理
}
- 适用场景*:需要严格保证顺序(如流水线操作)、下游服务扛不住并发冲击时。
解法2:Promise.all控制并发量(性能和稳定的平衡点)
javascript
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等库实现高级控制
javascript
import pMap from 'p-map';
await pMap(users, updateUserStatus, { concurrency: 100 }); // 精确控制并发数
- 推荐库 *:
p-map、async-pool。这些库帮你处理了重试、错误处理等脏活。
4. 避坑清单:forEach的七个致命陷阱
- 无视异步 :回调内的
await不会暂停遍历(与map/filter等数组方法行为一致); - 无法中断 :即使回调内
throw error,其他回调依然会继续执行(相比之下for...of可用break提前退出); - 性能幻觉:你以为的"并行优化"可能变成DDOS攻击;
- 副作用传染:在回调内修改外部变量时,可能因执行顺序不确定导致竞态条件;
- 无返回值 :与
map不同,forEach永远返回undefined,无法链式调用; - 稀疏数组陷阱 :对
[1,,3]这样的数组,空元素会直接被跳过(但for...of会遍历到undefined); - 猴子补丁风险 :如果有人在
Array.prototype.forEach上动了手脚...(没错,我见过有人这么干)。
5. 什么时候该用forEach?
只有一种情况我会用它:同步、无副作用的纯遍历。比如:
javascript
// 打印日志、渲染UI等不需要等待的操作
logs.forEach(log => console.log(log));
- 最后送大家一句话 *:在JavaScript的异步世界里,
forEach是个好员工,但它只会埋头干活,从不抬头看路------你需要明确告诉它"该怎么走"。
你在项目里是怎么处理批量异步任务的?遇到过forEach的其他坑吗?欢迎评论区聊聊你的实战经历。