在循環(huán)中使用 await,代碼看似直觀,但運(yùn)行時(shí)要么悄無(wú)聲息地停止,要么運(yùn)行速度緩慢,這是為什么呢?
本篇聊聊 JavaScript 中的異步循環(huán)問(wèn)題。
2. 踩坑 1:for 循環(huán)里用 await,效率太低
假設(shè)要逐個(gè)獲取用戶(hù)數(shù)據(jù),可能會(huì)這樣寫(xiě):
const users = [1, 2, 3];
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
代碼雖然能運(yùn)行,但會(huì)順序執(zhí)行——必須等 fetchUser(1) 完成,fetchUser(2) 才會(huì)開(kāi)始。
若業(yè)務(wù)要求嚴(yán)格按順序執(zhí)行,這樣寫(xiě)沒(méi)問(wèn)題;但如果請(qǐng)求之間相互獨(dú)立,這種寫(xiě)法就太浪費(fèi)時(shí)間了。
3. 踩坑 2:map 里直接用 await,拿到的全是 Promise
很多人會(huì)在 map() 里用 await,卻未處理返回的 Promise,結(jié)果踩了坑:
const users = [1, 2, 3];
const results = users.map(async (id) => {
const user = await fetchUser(id);
return user;
});
console.log(results); // 輸出 [Promise, Promise, Promise],而非實(shí)際用戶(hù)數(shù)據(jù)
語(yǔ)法上沒(méi)問(wèn)題,但它不會(huì)等 Promise resolve。若想讓請(qǐng)求并行執(zhí)行并獲取最終結(jié)果,需用 Promise.all():
const results = await Promise.all(users.map((id) => fetchUser(id)));
這樣所有請(qǐng)求會(huì)同時(shí)發(fā)起,results 中就是真正的用戶(hù)數(shù)據(jù)了。
4. 踩坑 3:Promise.all 一錯(cuò)全錯(cuò)
用 Promise.all() 時(shí),只要有一個(gè)請(qǐng)求失敗,整個(gè)操作就會(huì)報(bào)錯(cuò):
代碼高亮:
const results = await Promise.all(
users.map((id) => fetchUser(id)) // 假設(shè) fetchUser(2) 出錯(cuò)
);
如果 fetchUser(2) 返回 404 或網(wǎng)絡(luò)錯(cuò)誤,Promise.all() 會(huì)直接 reject,即便其他請(qǐng)求成功,也拿不到任何結(jié)果。
5. 更安全的替代方案
5.1. 用 Promise.allSettled(),保留所有結(jié)果
使用 Promise.allSettled(),即便部分請(qǐng)求失敗,也能拿到所有結(jié)果,之后可手動(dòng)判斷成功與否:
const results = await Promise.allSettled(users.map((id) => fetchUser(id)));
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log("? 用戶(hù)數(shù)據(jù):", result.value);
} else {
console.warn("? 錯(cuò)誤:", result.reason);
}
});
?
5.2. 在 map 里加 try/catch,返回兜底值
也可在請(qǐng)求時(shí)直接捕獲錯(cuò)誤,給失敗的請(qǐng)求返回默認(rèn)值:
const results = await Promise.all(
users.map(async (id) => {
try {
return await fetchUser(id);
} catch (err) {
console.error(`獲取用戶(hù)${id}失敗`, err);
return { id, name: "未知用戶(hù)" }; // 兜底數(shù)據(jù)
}
})
);
這樣還能避免 “unhandled promise rejections” 錯(cuò)誤——在 Node.js 嚴(yán)格環(huán)境下,該錯(cuò)誤可能導(dǎo)致程序崩潰。
6. 現(xiàn)代異步循環(huán)方案,按需選擇
6.1. for...of + await:適合需順序執(zhí)行的場(chǎng)景
若下一個(gè)請(qǐng)求依賴(lài)上一個(gè)的結(jié)果,或需遵守 API 的頻率限制,可采用此方案:
// 在 async 函數(shù)內(nèi)
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
// 不在 async 函數(shù)內(nèi),用立即執(zhí)行函數(shù)
(async () => {
for (const id of users) {
const user = await fetchUser(id);
console.log(user);
}
})();
6.2. Promise.all + map:適合追求速度的場(chǎng)景
請(qǐng)求間相互獨(dú)立且可同時(shí)執(zhí)行時(shí),此方案效率最高:
代碼高亮:
const usersData = await Promise.all(users.map((id) => fetchUser(id)));
6.3. 限流并行:用 p-limit 控制并發(fā)數(shù)
若需兼顧速度與 API 限制,可借助 p-limit 等工具控制同時(shí)發(fā)起的請(qǐng)求數(shù)量:
import pLimit from "p-limit";
const limit = pLimit(2); // 每次同時(shí)發(fā)起 2 個(gè)請(qǐng)求
const limitedFetches = users.map((id) => limit(() => fetchUser(id)));
const results = await Promise.all(limitedFetches);
7. 注意:千萬(wàn)別在 forEach() 里用 await
這是個(gè)高頻陷阱:
users.forEach(async (id) => {
const user = await fetchUser(id);
console.log(user); // ? 不會(huì)等待執(zhí)行完成
});
forEach() 不會(huì)等待異步回調(diào),請(qǐng)求會(huì)在后臺(tái)亂序執(zhí)行,可能導(dǎo)致代碼邏輯出錯(cuò)、錯(cuò)誤被遺漏。
替代方案:
8. 總結(jié):按需選擇
JavaScript 異步能力很強(qiáng),但循環(huán)里用 await 要“按需選擇”,核心原則如下:

參考文章:原文鏈接?