原文链接:allthingssmitty.com/2025/10/20/…
作者:Matt Smith
在循环中使用 await 看似直观,直到你的代码悄然卡住或运行速度低于预期。若你曾疑惑为何 API 调用会逐个执行而非同时进行,或为何 map() 与 await 的配合效果与预期不符——请坐下,我们聊聊。
问题:在 for 循环中等待
假设你正在逐个获取用户列表:
const users = [1, 2, 3];
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
这段代码能运行,但采用顺序执行:fetchUser(2) 需等到 fetchUser(1) 完成后才会启动。若操作顺序重要则无妨,但对独立的网络请求而言效率低下。
除非有意为之,否则不要在 map() 内使用 await
常见误区是在 map() 内使用 await 却未处理生成的 Promise:
const users = [1, 2, 3];
const results = users.map(async id => {
const user = await fetchUser(id);
return user;
});
console.log(results); // [Promise, Promise, Promise] – NOT actual user data
从语法和行为上来说(它返回一个承诺数组),这段代码是有效的,但并非多数人预期的那样工作。它不会等待承诺解析完成。
要并行执行调用并获取最终结果:
const results = await Promise.all(users.map(id => fetchUser(id)));
现在所有请求都并行运行,结果包含实际获取的用户。
Promise.all() 会快速失败,即使只有一次调用失败
使用 Promise.all() 时,单次拒绝会导致整个操作失败:
const results = await Promise.all(
users.map(id => fetchUser(id)) // fetchUser(2) might throw
);
如果 fetchUser(2) 抛出错误(例如 404 或网络错误),整个 Promise.all 调用将被拒绝,且不会返回任何结果(包括成功返回的结果)。
⚠️ 注意:
Promise.all()在遇到首个错误时即会拒绝,并丢弃其他结果。剩余的 Promise 仍会继续执行,但除非单独处理每个结果,否则仅会报告首个拒绝状态。
更安全的替代方案
使用 Promise.allSettled()
const results = await Promise.allSettled(
users.map(id => fetchUser(id))
);
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('✅ User:', result.value);
} else {
console.warn('❌ Error:', result.reason);
}
});
当您需要处理所有结果时使用此方法,即使部分结果失败。
在映射函数内部处理错误
const results = await Promise.all(
users.map(async id => {
try {
return await fetchUser(id);
} catch (err) {
console.error(`Failed to fetch user ${id}`, err);
return { id, name: 'Unknown User' }; // fallback value
}
})
);
这同时能防止未处理的 Promise 拒绝,在 Node.js 等采用 --unhandled-rejections=strict 参数的严格环境中,此类拒绝可能触发警告或导致进程崩溃。
现代解决方案
使用 for...of + await(顺序执行)
适用于:
- 下一步操作依赖前一步结果时
- API 速率限制要求时
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);
}
})();
- 保持顺序
- 适用于速率限制或批处理
- 独立请求时速度较慢
使用 Promise.all + map()(并行执行)
当操作相互独立且可同时执行时使用:
const usersData = await Promise.all(users.map(id => fetchUser(id)));
- 对于网络密集型或CPU无关的任务,速度显著提升
- 单次拒绝将导致整个批次失败(除非进行处理)
使用Promise.allSettled()或内联try/catch实现更安全的批量执行。
对于短暂的CPU密集型任务,并行处理可能不会带来明显差异。但对于API调用等I/O密集型操作,并行处理可显著缩短总执行时间。
限流并行处理(受控并发)
当需要提升速度但必须遵守API限制时,请使用限流工具如p-limit:
import pLimit from 'p-limit';
const limit = pLimit(2); // Run 2 fetches at a time
const limitedFetches = users.map(id => limit(() => fetchUser(id)));
const results = await Promise.all(limitedFetches);
- 并发与控制之间的平衡
- 防止外部服务过载
- 增加依赖性
💡 深入探索
若想了解 await 在函数外部的具体行为,请参阅我关于在 ES 模块中使用顶级 await 的文章。
并发级别
| 目标 | 模式 | 并发性 |
|---|---|---|
| 保持顺序,依次执行 | for...of + await | 1 |
| 同时执行,无顺序 | Promise.all() + map() | ∞ (无界) ✅ |
| 限制并发性 | p-limit, PromisePool, etc. | N (自定义) |
最后一个技巧:永远不要在 forEach() 中使用 await
这是个常见陷阱:
users.forEach(async id => {
const user = await fetchUser(id);
console.log(user); // ❌ Not awaited
});
该循环不会等待async函数执行完毕。这些请求在后台运行,无法保证完成时间或顺序。
⚠️ 注意:
forEach不会等待异步回调。你的函数可能在异步任务完成前就结束,导致隐性错误和遗漏的异常。
建议替代方案:
for...of+await实现顺序逻辑Promise.all()+map()实现并行逻辑
🙋🏻♂️ 准备好深入学习?
想用更函数式的方式处理异步迭代?
Array.fromAsync()专为处理流和生成器等异步数据源而设计。
快速回顾
JavaScript 的异步模型功能强大,但在循环中使用 await 需要刻意为之。关键在于:根据需求构建异步逻辑。
- 顺序 →
for...of - 速度 →
Promise.all() - 安全性 →
allSettled()/try-catch - 平衡性 →
p-limit等
采用正确的模式,可编写更快、更安全、更可预测的异步代码。