Featured image of post JavaScript 非同步/等待應避免的模式Featured image of post JavaScript 非同步/等待應避免的模式

JavaScript 非同步/等待應避免的模式

探索 JavaScript 中常見的非同步/等待反模式,並了解如何優化非同步執行以獲得更好的效能和錯誤處理。

介紹

自從 ES2017 中引入 async/await 以來,編寫非同步 JavaScript 變得更加直觀。它允許開發人員以看起來同步的結構表達非同步邏輯,與嵌套的 Promise 鏈 (.then().catch()) 相比,大大提高了程式碼庫的可讀性。

然而,這種乾淨的類似同步的語法可以隱藏效能缺陷和錯誤處理錯誤。 在本文中,我們將深入研究常見的非同步/等待反模式,並學習如何將它們重構為高效能、有彈性的程式碼。


反模式 1:不必要的序列化(順序等待)

最常見的錯誤是順序執行獨立的非同步任務,錯過了並行執行的機會。

應避免的程式碼

// Fetching three independent datasets sequentially
async function getUserProfile(userId) {
  const user = await fetchUser(userId);       // takes 1.0s
  const posts = await fetchPosts(userId);     // takes 1.5s
  const followers = await fetchFollowers(userId); // takes 0.8s

  return { user, posts, followers };
}

在這裡,fetchPostsfetchUser 完成之前不會開始,並且 fetchFollowers 被兩者阻止。總執行時間為 1.0 + 1.5 + 0.8 = 3.3 seconds。由於這些請求彼此不依賴,因此應該同時請求它們。

重構程式碼(使用 Promise.all)

async function getUserProfile(userId) {
  // Fire requests concurrently and wait for all to resolve
  const [user, posts, followers] = await Promise.all([
    fetchUser(userId),
    fetchPosts(userId),
    fetchFollowers(userId)
  ]);

  return { user, posts, followers };
}

使用 Promise.all 會同時觸發所有三個提取。總等待時間降至最長請求的持續時間:1.5 秒


反模式 2:嵌套的 try-catch 區塊

在處理潛在的 API 故障時,一些開發人員會恢復到非同步函數內的深度嵌套模式,模仿傳統的回調地獄。

應避免的程式碼

async function processData() {
  try {
    const rawData = await fetchData();
    try {
      const parsedData = await parseData(rawData);
      try {
        await saveToDatabase(parsedData);
      } catch (dbError) {
        console.error("Database save failed", dbError);
      }
    } catch (parseError) {
      console.error("Parsing failed", parseError);
    }
  } catch (fetchError) {
    console.error("Fetch failed", fetchError);
  }
}

深度嵌套違背了 async/await 的目的,使得函數難以掃描和維護。

重構程式碼

如果您要擷取管道期間的任何錯誤,請將整個流程包裝在單一頂層 try-catch 區塊中。如果您必須唯一地處理特定錯誤,請將它們在 catch 區塊中分類或將 .catch() 處理程序附加到各個 Promise。

async function processData() {
  try {
    const rawData = await fetchData();
    const parsedData = await parseData(rawData);
    await saveToDatabase(parsedData);
  } catch (error) {
    // Handle or categorize errors centrally
    handleProcessingError(error);
  }
}

反模式 3:在 Array.prototype.forEach() 內使用非同步回呼

forEach 與非同步回呼一起使用將在編譯時沒有語法錯誤,但非同步任務將無序運行。

應避免的程式碼

// Iterating and sending emails asynchronously
async function sendEmails(users) {
  users.forEach(async (user) => {
    await mailer.send(user.email);
  });
  console.log("All emails queued or sent!"); // <- Executes before mails actually finish sending!
}

Array.prototype.forEach 不等待其回呼函數傳回的承諾的解決。它同步觸發所有回調。因此,後續日誌語句會在任何電子郵件實際到達收件者之前立即列印。

重構程式碼(使用 for…of 或 Promise.all)

如果任務必須依序執行(一項一項):

async function sendEmailsSerially(users) {
  for (const user of users) {
    await mailer.send(user.email); // awaits each send
  }
  console.log("All emails sent successfully.");
}

如果任務應該同時運行:

async function sendEmailsInParallel(users) {
  const promises = users.map(user => mailer.send(user.email));
  await Promise.all(promises); // waits for all promises to resolve
  console.log("All emails sent successfully.");
}

反模式 4:不含await 關鍵字的非同步函數

當函數僅包含同步操作時將其宣告為 async 會造成不必要的效能消耗。

應避免的程式碼

// Simple synchronous math calculation
async function calculateSum(a, b) {
  return a + b;
}

當您為函數新增前綴 async 時,執行時間引擎會自動將回傳值包裝在已解析的 Promise 中。這會增加少量的運行時開銷(微任務佇列調度和記憶體分配)。如果函數內部沒有 await ,則保持同步。


結論

Async/await 為編寫非同步 JavaScript 提供了優雅的語法,但它仍然是基於 Promises 建構的。請記住這些規則:

  1. **使用 Promise.all 组合独立任务。 **
  2. **切勿將 forEach 與非同步回呼一起使用;使用 for...ofPromise.all 取代。 **
  3. **避免巢狀 try-catch 區塊,以保持程式碼可讀且易於偵錯。 **