掌握 JavaScript async/await:從入門到進階的完整指南 2025

作者: Calpa Liu
字數:2021
出版:2025年3月30日
分類: JavaScript PromisesCallbacks效能優化最佳實踐

深入剖析 JavaScript 非同步程式設計的最新發展!從基礎概念到進階應用,本文完整介紹 async/await 的強大功能、最佳實踐和效能優化技巧。透過實用範例,了解如何運用 async/await 提升程式碼品質並避免常見陷阱。

非同步程式設計的演進

JavaScript 是一種單線程語言,為了避免長時間運行的操作阻塞用戶界面,非同步程式設計成為必要。在 JavaScript 的發展歷史中,處理非同步操作的方式經歷了三個主要階段:

回調函數 (Callbacks)

最初,JavaScript 透過回調函數處理非同步操作。然而,當處理多個相互依賴的非同步操作時,常常導致所謂的「回調地獄」(Callback Hell),造成程式碼可讀性和維護性的挑戰。

Promises

ES2015 (ES6) 引入了 Promise 物件,提供了一種更結構化的方式來處理非同步操作。Promise 是代表未來會產生值的第一類物件,可以輕鬆地傳遞、組合和轉換。透過 .then().catch() 方法,它改善了回調地獄的問題,但仍有其局限性。

async/await

ES2017 引入了 async/await 語法,建立在 Promise 的基礎上,提供了更直觀的方式來編寫和理解非同步程式碼。async/await 允許開發者以看似同步的方式編寫非同步程式碼,大大提升了程式碼的可讀性和維護性。

async/await 的基本概念與語法

async/await 由兩個關鍵字組成:

  1. async:用於宣告一個非同步函數,該函數將始終返回一個 Promise。
  2. await:只能在 async 函數內部使用,用於暫停函數執行,直到 Promise 解決。

基本語法示例:

async function fetchData(url) {
  try {
    const response = await fetch(url);
    const data = await response.json();
    return data;
  } catch (error) {
    console.error(error);
  }
}

async/await 相較於 Promises 的優勢

1. 更清晰的程式碼結構與可讀性

async/await 使非同步程式碼看起來與同步程式碼相似,減少了鏈式調用的複雜性,使程式碼更容易閱讀和理解。

使用 Promise:

const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data);
      return "done";
    });

使用 async/await:

const makeRequest = async () => {
  console.log(await getJSON());
  return "done";
};

這種結構使得程式碼更加線性和直觀,特別是在處理多個非同步操作時。

2. 更自然的錯誤處理

async/await 允許使用傳統的 try/catch 結構來處理同步和非同步錯誤,使錯誤處理更加一致和直觀。

使用 Promise:

const makeRequest = () => {
  try {
    getJSON()
      .then(result => {
        const data = JSON.parse(result);
        console.log(data);
      });
  } catch (err) {
    console.log(err);
  }
};

使用 async/await:

const makeRequest = async () => {
  try {
    const data = JSON.parse(await getJSON());
    console.log(data);
  } catch (err) {
    console.log(err);
  }
};

在 Promise 版本中,try/catch 塊無法捕獲 Promise 內部的錯誤,而在 async/await 版本中,所有錯誤都能被適當捕獲。

3. 更容易的調試體驗

使用 async/await 進行調試比調試 Promises 更加容易。在調試 Promises 時,有兩個主要問題:

  1. 無法在返回表達式的箭頭函數中設置斷點
  2. 當使用斷點調試步進功能時,調試器不會移動到下一個 .then 塊,因為它只步進同步程式碼

相比之下,使用 async/await,可以像調試同步程式碼一樣,設置斷點並步進執行非同步操作。

4. 更優雅地處理條件邏輯

async/await 使處理條件邏輯變得更加簡潔。當需要基於前一個非同步調用的結果進行條件操作時,Promise 鏈變得複雜且難以閱讀。

使用 async/await:

const makeRequest = async () => {
  const data = await getJSON();
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    console.log(moreData);
    return moreData;
  } else {
    console.log(data);
    return data;
  }
};

這種方式比使用 Promises 的嵌套 .then() 調用更清晰易讀。

5. 更簡單地處理中間值

在需要多個非同步操作並且後續操作依賴前一個操作結果的情況下,async/await 特別有用。

使用 async/await:

const makeRequest = async () => {
  const value1 = await promise1();
  const value2 = await promise2(value1);
  return promise3(value1, value2);
};

與嵌套的 Promise 鏈相比,這種代碼結構顯著簡化了複雜的非同步流程。

async/await 的進階特性與效能優化

Top-level await

ES2022 引入了 top-level await,允許在模組頂層使用 await,無需包裝在 async 函數中:

// config.js
const config = await fetch('/api/config');
export default await config.json();

AbortController 整合

async/await 可以與 AbortController 完美配合,實現請求取消功能:

const controller = new AbortController();
const { signal } = controller;

const fetchData = async () => {
  try {
    const response = await fetch('https://api.example.com/data', { signal });
    const data = await response.json();
    return data;
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('請求已取消');
    } else {
      throw error;
    }
  }
};

// 5 秒後取消請求
setTimeout(() => controller.abort(), 5000);

async/await 在應用中的效能與擴展性

提升 Web 應用的效能與擴展性

記憶體管理優化

使用 async/await 時,需要注意記憶體管理。以下是一些優化建議:

// 不佳的做法 - 可能導致記憶體洩漏
const processItems = async (items) => {
  const results = [];
  for (const item of items) {
    results.push(await processItem(item));
  }
  return results;
};

// 優化後的做法 - 使用 for...of 進行迭代
const processItems = async (items) => {
  const results = [];
  for await (const item of items) {
    const result = await processItem(item);
    results.push(result);
  }
  return results;
};

// 更好的做法 - 使用 Promise.all 並控制並發數
const processItemsInBatches = async (items, batchSize = 5) => {
  const results = [];
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    const batchResults = await Promise.all(
      batch.map(item => processItem(item))
    );
    results.push(...batchResults);
  }
  return results;
};

效能監控

在生產環境中監控 async/await 的效能:

const measureAsyncOperation = async (operation) => {
  const start = performance.now();
  try {
    const result = await operation();
    const duration = performance.now() - start;
    console.log(`操作完成,耗時:${duration}ms`);
    return result;
  } catch (error) {
    const duration = performance.now() - start;
    console.error(`操作失敗,耗時:${duration}ms`, error);
    throw error;
  }
};

在 Web 應用程序中,特別是服務器端應用(如 ASP.NET),async/await 能夠顯著提高應用程序的擴展性。當執行 I/O 密集型操作(如數據庫查詢或 API 調用)時,使用 async/await 可以釋放線程,使其能夠處理其他請求,從而提高整體吞吐量。

更有效地處理多個非同步操作

async/await 與 Promise.all() 結合使用時,可以有效地並行處理多個非同步操作。

const makeRequest = async () => {
  const [value1, value2, value3] = await Promise.all([
    promise1(),
    promise2(),
    promise3()
  ]);
  return processValues(value1, value2, value3);
};

2025 年的最佳實踐與注意事項

新的錯誤處理模式

使用新的 Error Cause API 提供更詳細的錯誤信息:

const fetchUserData = async (userId) => {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new Error('獲取用戶數據失敗', {
        cause: {
          code: response.status,
          url: response.url,
          userId
        }
      });
    }
    return await response.json();
  } catch (error) {
    console.error('錯誤詳情:', error.cause);
    throw error;
  }
};

使用結構化並發

const processUserData = async () => {
  try {
    const results = await Promise.allSettled([
      fetchUserProfile(),
      fetchUserPosts(),
      fetchUserPreferences()
    ]);
    
    const [profile, posts, preferences] = results.map(result => 
      result.status === 'fulfilled' ? result.value : null
    );
    
    return {
      profile,
      posts,
      preferences,
      timestamp: new Date()
    };
  } catch (error) {
    console.error('處理用戶數據時發生錯誤:', error);
    throw error;
  }
};

何時使用 async/await

  • I/O 密集型操作,如網路請求、檔案存取和資料庫操作
  • 處理複雜的非同步流程和依賴關係
  • 需要清晰錯誤處理的情況

何時避免使用 async/await

  • CPU 密集型操作可能導致效能下降
  • 過度使用可能會不必要地增加複雜性
  • 當不需要等待操作完成時,可能不需要非同步處理

常見陷阱

  • 忘記在函數前添加 async 關鍵字
  • 在非同步函數中忘記使用 await
  • 產生不必要的序列化操作,而不是使用 Promise.all() 進行並行處理

結論

async/await 不僅僅是 Promise 的語法糖,而是提供了真正的優勢,使非同步程式碼更具表現力、可讀性和可維護性。它統一了非同步程式設計的經驗,提供了更好的錯誤堆疊追蹤,並允許使用所有同步程式設計中可用的語言結構。

隨著 Web 開發不斷向更複雜的應用演進,掌握 async/await 成為現代 JavaScript 開發者的必備技能。對於需要處理大量非同步操作的專案,使用 async/await 可以顯著提高程式碼質量和開發效率。

雖然 async/await 提供了許多優勢,但理解其底層原理以及與 Promises 的關係仍然重要,這有助於在各種情況下選擇最合適的非同步處理方式。

感謝您閱讀我的文章。歡迎隨時分享你的想法。
JavaScript PromisesCallbacks效能優化最佳實踐
關於 Calpa

Calpa 擅長使用 TypeScriptReact.jsVue.js 建立 Responsive Website。

他積極參與開源社區,曾在 2019 年的香港開源大會上擔任講者,提供工作經驗和見解。此外,他也在 GitHub 上公開分享個人博客程式碼,已獲得超過 300 顆星星和 60 個分支的支持。

他熱愛學習新技術,並樂意分享經驗。他相信,唯有不斷學習才能跟上快速演變的技術環境。