1. 前言
在循环中使用 await,代码看似直观,但运行时要么悄无声息地停止,要么运行速度缓慢,这是为什么呢?
本篇聊聊 JavaScript 中的异步循环问题。
2. 踩坑 1:for 循环里用 await,效率太低
假设要逐个获取用户数据,可能会这样写:
javascript
const users = [1, 2, 3];
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
代码虽然能运行,但会顺序执行 ------必须等 fetchUser(1) 完成,fetchUser(2) 才会开始。若业务要求严格按顺序执行,这样写没问题;但如果请求之间相互独立,这种写法就太浪费时间了。
3. 踩坑 2:map 里直接用 await,拿到的全是 Promise
很多人会在 map() 里用 await,却未处理返回的 Promise,结果踩了坑:
javascript
const users = [1, 2, 3];
const results = users.map(async (id) => {
const user = await fetchUser(id);
return user;
});
console.log(results); // 输出 [Promise, Promise, Promise],而非实际用户数据
语法上没问题,但它不会等 Promise resolve。若想让请求并行执行并获取最终结果,需用 Promise.all():
javascript
const results = await Promise.all(users.map((id) => fetchUser(id)));
这样所有请求会同时发起 ,results 中就是真正的用户数据了。
4. 踩坑 3:Promise.all 一错全错
用 Promise.all() 时,只要有一个请求失败,整个操作就会报错:
javascript
const results = await Promise.all(
users.map((id) => fetchUser(id)) // 假设 fetchUser(2) 出错
);
如果 fetchUser(2) 返回 404 或网络错误,Promise.all() 会直接 reject,即便其他请求成功,也拿不到任何结果。
5. 更安全的替代方案
5.1. 用 Promise.allSettled(),保留所有结果
使用 Promise.allSettled(),即便部分请求失败,也能拿到所有结果,之后可手动判断成功与否:
javascript
const results = await Promise.allSettled(users.map((id) => fetchUser(id)));
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("✅ 用户数据:", result.value);
} else {
console.warn("❌ 错误:", result.reason);
}
});
5.2. 在 map 里加 try/catch,返回兜底值
也可在请求时直接捕获错误,给失败的请求返回默认值:
javascript
const results = await Promise.all(
users.map(async (id) => {
try {
return await fetchUser(id);
} catch (err) {
console.error(`获取用户${id}失败`, err);
return { id, name: "未知用户" }; // 兜底数据
}
})
);
这样还能避免 "unhandled promise rejections" 错误------在 Node.js 严格环境下,该错误可能导致程序崩溃。
6. 现代异步循环方案,按需选择
6.1. for...of + await:适合需顺序执行的场景
若下一个请求依赖上一个的结果,或需遵守 API 的频率限制,可采用此方案:
javascript
// 在 async 函数内
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
// 不在 async 函数内,用立即执行函数
(async () => {
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
})();
- 优点:保证顺序,支持限流
- 缺点:独立请求场景下速度慢
6.2. Promise.all + map:适合追求速度的场景
请求间相互独立且可同时执行时,此方案效率最高:
javascript
const usersData = await Promise.all(users.map((id) => fetchUser(id)));
- 优点:网络请求、CPU 独立任务场景下速度快
- 缺点:一个请求失败会导致整体失败(需手动处理错误)
6.3. 限流并行:用 p-limit 控制并发数
若需兼顾速度与 API 限制,可借助 p-limit 等工具控制同时发起的请求数量:
javascript
import pLimit from "p-limit";
const limit = pLimit(2); // 每次同时发起 2 个请求
const limitedFetches = users.map((id) => limit(() => fetchUser(id)));
const results = await Promise.all(limitedFetches);
- 优点:平衡并发和控制,避免压垮外部服务
- 缺点:需额外引入依赖
7. 注意:千万别在 forEach() 里用 await
这是个高频陷阱:
javascript
users.forEach(async (id) => {
const user = await fetchUser(id);
console.log(user); // ❌ 不会等待执行完成
});
forEach() 不会等待异步回调,请求会在后台乱序执行,可能导致代码逻辑出错、错误被遗漏。
替代方案:
- 顺序执行:用 for...of + await
- 并行执行:用 Promise.all() + map()
8. 总结:按需选择
JavaScript 异步能力很强,但循环里用 await 要"按需选择",核心原则如下:
| 需求场景 | 推荐方案 |
|---|---|
| 需保证顺序、逐个执行 | for...of + await |
| 追求速度、独立请求 | Promise.all() + map() |
| 需保留所有结果(含失败) | Promise.allSettled()/try-catch |
| 需控制并发数、遵守限流 | p-limit 等工具 |