動態

詳情 返回 返回

JavaScript 異步循環踩坑指南 - 動態 詳情

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. 參考鏈接

  1. https://allthingssmitty.com/2025/10/20/rethinking-async-loops-in-javascript/
user avatar qq15725 頭像 imhaoli 頭像
點贊 2 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.