在循環中使用 await,程式碼看似直觀,但執行時要麼悄無聲息地停止,要麼運行速度緩慢,這是為什麼呢?
本篇聊聊 JavaScript 中的非同步循環問題。
假設要逐個獲取用戶資料,可能會這樣寫:
const users = [1, 2, 3];
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
程式碼雖然能運行,但會順序執行——必須等 fetchUser(1) 完成,fetchUser(2) 才會開始。若業務要求嚴格按順序執行,這樣寫沒問題;但如果請求之間相互獨立,這種寫法就太浪費時間了。
很多人會在 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 中就是真正的用戶資料了。
用 Promise.all() 時,只要有一個請求失敗,整個操作就會報錯:
const results = await Promise.all(
users.map((id) => fetchUser(id)) // 假設 fetchUser(2) 出錯
);
如果 fetchUser(2) 返回 404 或網路錯誤,Promise.all() 會直接 reject,即便其他請求成功,也拿不到任何結果。
使用 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);
}
});
也可在請求時直接捕獲錯誤,給失敗的請求返回預設值:
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 嚴格環境下,該錯誤可能導致程式崩潰。
若下一個請求依賴上一個的結果,或需遵守 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);
}
})();
請求間相互獨立且可同時執行時,此方案效率最高:
const usersData = await Promise.all(users.map((id) => fetchUser(id)));
若需兼顧速度與 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);
這是一個高頻陷阱:
users.forEach(async (id) => {
const user = await fetchUser(id);
console.log(user); // ❌ 不會等待執行完成
});
forEach() 不會等待非同步回調,請求會在背景亂序執行,可能導致程式碼邏輯出錯、錯誤被遺漏。
替代方案:
JavaScript 非同步能力很強,但循環裡用 await 要“按需選擇”,核心原則如下:
| 需求場景 | 推薦方案 |
|---|---|
| 需保證順序、逐個執行 | for...of + await |
| 追求速度、獨立請求 | Promise.all() + map() |
| 需保留所有結果(含失敗) | Promise.allSettled()/try-catch |
| 需控制並發數、遵守限流 | p-limit 等工具 |