1. 前言
在循環中使用 await,代碼看似直觀,但運行時要麼悄無聲息地停止,要麼運行速度緩慢,這是為什麼呢?
本篇聊聊 JavaScript 中的異步循環問題。
2. 踩坑 1:for 循環裏用 await,效率太低
假設要逐個獲取用户數據,可能會這樣寫:
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,結果踩了坑:
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():
const results = await Promise.all(users.map((id) => fetchUser(id)));
這樣所有請求會同時發起,results 中就是真正的用户數據了。
4. 踩坑 3:Promise.all 一錯全錯
用 Promise.all() 時,只要有一個請求失敗,整個操作就會報錯:
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(),即便部分請求失敗,也能拿到所有結果,之後可手動判斷成功與否:
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,返回兜底值
也可在請求時直接捕獲錯誤,給失敗的請求返回默認值:
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 的頻率限制,可採用此方案:
// 在 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:適合追求速度的場景
請求間相互獨立且可同時執行時,此方案效率最高:
const usersData = await Promise.all(users.map((id) => fetchUser(id)));
- 優點:網絡請求、CPU 獨立任務場景下速度快
- 缺點:一個請求失敗會導致整體失敗(需手動處理錯誤)
6.3. 限流並行:用 p-limit 控制併發數
若需兼顧速度與 API 限制,可藉助 p-limit 等工具控制同時發起的請求數量:
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
這是個高頻陷阱:
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 等工具 |
9. 參考鏈接
- https://allthingssmitty.com/2025/10/20/rethinking-async-loops-in-javascript/