Post

JavaScript Callback、Promise、Async Await

在上一篇文章中,我們探討了 JavaScript 同步與非同步。這篇文章將進一步介紹 JavaScript 中實現非同步操作的三種主要方式:CallbackPromiseAsync Await。這些技術各有優劣,適用於不同的場景和需求。

Callback

概念

當某事發生時,請利用這個函數通知我。就像當客戶打給你時,你正在跟別人通電話,所以你會請客戶稍等,等你講完這通電話再回撥給他。

例子:

  • 點擊事件:「當有人點擊這顆按鈕時,請用這個函數通知我」

    1
    2
    3
    
    document.getElementById("id").addEventListener("click", function() {
      alert("這是 callback");
    });
    
  • 定時器:「當過了三秒時,請用這個函數通知我」

    1
    2
    3
    
    setTimeout(function() {
      alert("這是 callback");
    }, 3000);
    
  • window.onload:「當網頁載入完成時,請用這個函數通知我」

    1
    2
    3
    
    window.onload = function() {
      alert("這是 callback");
    };
    

優點:

  • 簡單直觀:Callback 函數的概念簡單,易於理解和使用

缺點:

  • Callback 地獄:當多層 Callback 嵌套時,程式碼變得難以閱讀和維護
  • 錯誤處理困難:需要手動處理每個 Callback 中的錯誤,容易出現疏漏

image

Promise

概念

Promise 是 ES6 引入的一種異步編程解決方案。它是一個代表未來某個值或事件的對象,可以處理異步操作的最終成功 resolved 或失敗 rejected 結果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = "資料已取得";
      resolve(data);
    }, 1000);
  });
}

fetchData()
  .then((data) => {
    console.log("處理資料:", data);
  })
  .catch((error) => {
    console.error("發生錯誤:", error);
  });

優點

  • 鏈式調用:可以使用 .then() 進行鏈式調用,使程式碼更具可讀性
  • 錯誤處理更容易:可以使用 .catch() 方法統一處理錯誤

缺點

  • 相對複雜:相比 Callback ,Promise 的概念和使用稍微複雜一些,對新手不太友好
  • 不夠直觀:處理錯誤和追踪異步操作的狀態有時會變得繁瑣
  • 嵌套地獄:雖然 Promise 通常比 Callback 更易於閱讀,但如果使用不當,仍然可能導致多層嵌套,難以維護

Async Await

概念

asyncawait 是 ES8 引入的語法糖,用於簡化基於 Promise 的異步操作。async 函數會自動返回一個 Promise,而 await 關鍵字可以暫停函數的執行,等待 Promise 解決。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = "資料已取得";
      resolve(data);
    }, 1000);
  });
}

async function processData() {
  try {
    const data = await fetchData();
    console.log("處理資料:", data);
  } catch (error) {
    console.error("發生錯誤:", error);
  }
}

processData();

優點

  • 同步風格的代碼:使異步代碼看起來像同步代碼,增加可讀性
  • 錯誤處理簡單:可以使用 try...catch 進行錯誤處理

缺點

  • 需要理解 Promise:雖然語法簡化,但仍需理解 Promise 的概念

Best Practices

Callback Best Practices

避免回呼地獄

每個 Callback 都被拆分成單獨的函數,並且每個函數都具有描述性的名稱,使程式碼更易於閱讀和維護,但還是要避免太多層 Callback,會影響閱讀和 trace code 還有 error handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function firstTask(callback) {
  setTimeout(() => {
    console.log("第一個任務完成");
    callback();
  }, 1000);
}

function secondTask(callback) {
  setTimeout(() => {
    console.log("第二個任務完成");
    callback();
  }, 1000);
}

function thirdTask(callback) {
  setTimeout(() => {
    console.log("第三個任務完成");
    callback();
  }, 1000);
}

function executeTasks() {
  firstTask(() => {
    secondTask(() => {
      thirdTask(() => {
        console.log("所有任務完成");
      });
    });
  });
}

executeTasks();
錯誤處理

在 Callback 中處理錯誤並將錯誤傳遞給下一個回呼

1
2
3
4
5
6
7
8
9
10
function firstTask(callback) {
  setTimeout(() => {
    try {
      console.log("第一個任務完成");
      callback(null);
    } catch (error) {
      callback(error);
    }
  }, 1000);
}

Promise Best Practices

鏈式調用

利用 .then().catch() 進行鏈式調用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = "資料已取得";
      resolve(data);
    }, 1000);
  });
}

fetchData()
  .then((data) => {
    console.log("處理資料:", data);
    return anotherTask(data);
  })
  .catch((error) => {
    console.error("發生錯誤:", error);
  });
避免嵌套

避免嵌套,將多個 Promise 進行平行處理

1
2
3
4
5
6
7
Promise.all([fetchData(), anotherTask()])
  .then((results) => {
    console.log("所有任務完成:", results);
  })
  .catch((error) => {
    console.error("發生錯誤:", error);
  });

Async Await Best Practices

錯誤處理

使用 try...catch 來捕獲並處理錯誤

1
2
3
4
5
6
7
8
9
10
async function processData() {
  try {
    const data = await fetchData();
    console.log("處理資料:", data);
  } catch (error) {
    console.error("發生錯誤:", error);
  }
}

processData();
效能優化,同時進行異步操作

在非同步操作中,使用 Promise.all 同時處理多個異步操作,進一步提高效能

1
2
3
4
5
6
7
8
9
10
async function processMultipleTasks() {
  try {
    const [data1, data2] = await Promise.all([fetchData(), anotherTask()]);
    console.log("所有資料已取得:", data1, data2);
  } catch (error) {
    console.error("發生錯誤:", error);
  }
}

processMultipleTasks();
UI 反饋

在進行長時間的非同步操作時,應給使用者提供適當的反饋,如顯示加載動畫或提示訊息,改善使用者體驗

1
2
3
4
5
6
7
8
9
10
11
12
13
async function fetchDataWithFeedback() {
  try {
    showLoadingSpinner();
    const data = await fetchData();
    console.log("處理資料:", data);
  } catch (error) {
    console.error("發生錯誤:", error);
  } finally {
    hideLoadingSpinner();
  }
}

fetchDataWithFeedback();

參考資料

This post is licensed under CC BY 4.0 by the author.