Skip to main content

JavaScript 非同步處理方式

JavaScript 提供了多種處理非同步操作的方式,包括 Callback、Promise 和 Async/Await。每種方式都有其特點和適用場景。

Callback Function

Callback Function(回調函數) 是 JavaScript 中處理非同步操作最傳統的方式。它是一個作為參數傳遞給另一個函數的函數,當特定事件發生或操作完成時會被調用。

基本概念:

  1. 什麼是 Callback

    • 一個函數作為參數傳遞給另一個函數
    • 在特定條件滿足時被調用
    • 常用於處理非同步操作
  2. 基本語法

    function doSomething(callback) {
    // 執行某些操作
    const result = "操作完成";
    callback(result); // 調用回調函數
    }

    doSomething(function (result) {
    console.log(result); // "操作完成"
    });
  3. 常見應用場景

    • 事件處理
    • 定時器
    • AJAX 請求
    • 文件讀取

Callback 的優缺點:

優點

  • 簡單直觀
  • 廣泛支援
  • 不需要額外的語法

缺點

  • 容易產生「回調地獄」(Callback Hell)
  • 錯誤處理困難
  • 代碼可讀性差

回調地獄問題:

// 回調地獄示例
getData(function (a) {
getMoreData(a, function (b) {
getEvenMoreData(b, function (c) {
getFinalData(c, function (d) {
console.log(d);
});
});
});
});

Promise

Promise 本身是同步函數。在 JavaScript 中,當你創建一個 Promise 物件(例如使用 new Promise()),這是一個同步的操作。Promise 的構造函數會立即執行,並且傳入的執行器函數(executor function,即 (resolve, reject) => { ... })也會同步執行

詳細說明:

  1. Promise 構造函數是同步的: 當你執行 new Promise((resolve, reject) => { ... }) 時,執行器函數會立即運行,而不是等到事件循環的下一個 tick。這意味著執行器內的同步代碼會在當前調用棧中立即執行。

    console.log("開始");
    const promise = new Promise((resolve, reject) => {
    console.log("執行器內部");
    resolve("成功");
    });
    console.log("結束");

    輸出結果:

    開始
    執行器內部
    結束

    可以看到,執行器內部 的日誌會在 結束 之前打印,證明執行器是同步運行的。

  2. Promise 的狀態變化和回調是非同步的: 雖然 Promise 的創建和執行器是同步的,但當 resolvereject 被調用時,與之相關的 .then().catch().finally() 回調函數會被放入微任務隊列(microtask queue),並在當前調用棧清空後(即同步代碼執行完畢後)才會執行。這使得 Promise 的回調處理是非同步的。

    console.log("開始");
    const promise = new Promise((resolve, reject) => {
    console.log("執行器內部");
    resolve("成功");
    });
    promise.then((result) => {
    console.log("then 回調:", result);
    });
    console.log("結束");

    輸出結果:

    開始
    執行器內部
    結束
    then 回調: 成功

    這裡可以看到,.then 的回調是在同步代碼(開始結束)執行完後才執行的,這是因為 .then 的處理被推遲到微任務隊列。

  3. 總結

    • Promise 的創建和執行器函數是同步的。
    • Promise 的狀態變更(resolvereject)觸發的回調(如 .then.catch)是非同步的,會在微任務隊列中執行。
    • 這種設計使得 Promise 能夠在同步代碼中初始化,但其結果處理是非同步的,符合 JavaScript 的事件循環模型。

Async/Await

Async/Await 是 ES2017 引入的語法糖,讓非同步代碼看起來更像同步代碼,是處理 Promise 的更優雅方式。

基本概念:

  1. async 函數

    • 宣告一個函數為非同步函數
    • 自動返回一個 Promise
    • 可以使用 await 關鍵字
  2. await 關鍵字

    • 只能在 async 函數內使用
    • 等待 Promise 解決
    • 暫停函數執行直到 Promise 完成

基本語法:

// 宣告 async 函數
async function fetchData() {
try {
// 使用 await 等待 Promise
const response = await fetch("https://api.example.com/data");
const data = await response.json();
return data;
} catch (error) {
console.error("錯誤:", error);
}
}

// 調用 async 函數
fetchData().then((data) => {
console.log(data);
});

Async/Await 的優缺點:

優點

  • 代碼更易讀,類似同步代碼
  • 錯誤處理更簡單(使用 try/catch)
  • 避免回調地獄
  • 更好的調試體驗

缺點

  • 只能在 async 函數內使用
  • 可能讓代碼看起來是同步的,但實際上是非同步的
  • 需要較新的 JavaScript 環境支援

三種方式的比較

讓我用同一個例子來展示 Callback、Promise 和 Async/Await 的差異:

1. Callback 方式:

function fetchUserData(userId, callback) {
setTimeout(() => {
callback(null, { id: userId, name: "張三" });
}, 1000);
}

fetchUserData(1, (error, user) => {
if (error) {
console.error(error);
} else {
console.log(user);
}
});

2. Promise 方式:

function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: userId, name: "張三" });
}, 1000);
});
}

fetchUserData(1)
.then((user) => console.log(user))
.catch((error) => console.error(error));

3. Async/Await 方式:

function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: userId, name: "張三" });
}, 1000);
});
}

async function getUser() {
try {
const user = await fetchUserData(1);
console.log(user);
} catch (error) {
console.error(error);
}
}

getUser();

比較總結:

特性CallbackPromiseAsync/Await
可讀性差(回調地獄)中等好(類似同步代碼)
錯誤處理困難中等簡單(try/catch)
支援度廣泛廣泛需要較新環境
學習曲線簡單中等中等

實際應用建議

  1. 現代開發:推薦使用 Async/Await,代碼更清晰易讀
  2. 舊專案維護:可能需要使用 Callback 或 Promise
  3. API 設計:可以返回 Promise,讓使用者選擇使用 .then()await
  4. 錯誤處理:Async/Await 配合 try/catch 是最直觀的方式