JavaScript 基礎:非同步程式設計

2026-01-26 07:54 | By justin | JavaScript
(Updated: 2026-01-26 07:54)

JavaScript 基礎:非同步程式設計

各位好!

這篇是 JavaScript 系列的第九單元,我們要學習非同步(Asynchronous)程式設計。這是 JavaScript 最重要的特性之一,用於處理耗時操作。


什麼是非同步?

JavaScript 是單執行緒(single-threaded)的語言,一次只能做一件事。非同步讓我們可以「等待」某些操作,而不會阻塞程式執行。

// 同步:一件事做完才做下一件
console.log("開始");
console.log("處理中");
console.log("結束");
// 輸出:開始 → 處理中 → 結束(依序執行)

// 非同步:不等某件事做完就繼續
console.log("開始");

setTimeout(() => {
  console.log("處理中");
}, 2000);  // 2 秒後執行

console.log("結束");
// 輸出:開始 → 結束 → (2 秒後)處理中

// 類比:
// 同步 = 排隊點餐,前面的人點完才輪到你
// 非同步 = 拿號碼牌,可以先去坐著等叫號

為什麼需要非同步?

常見的非同步操作:

// 1. 計時器
setTimeout(() => {
  console.log("3 秒後執行");
}, 3000);

// 2. 網路請求
fetch("https://api.example.com/data")
  .then(response => response.json())
  .then(data => console.log(data));

// 3. 讀取檔案(Node.js)
// fs.readFile("file.txt", (err, data) => {
//   console.log(data);
// });

// 4. 使用者互動
// document.querySelector("button").addEventListener("click", () => {
//   console.log("按鈕被點擊");
// });

// 如果這些都是同步的,整個程式會卡住等待

回呼函式(Callback)

最基本的非同步處理方式。

// 計時器範例
function sayHello(name, callback) {
  setTimeout(() => {
    const message = `Hello, ${name}!`;
    callback(message);
  }, 1000);
}

sayHello("Alice", (message) => {
  console.log(message);  // 1 秒後:Hello, Alice!
});

// 實用範例:檔案處理模擬
function readFile(filename, callback) {
  console.log(`讀取 ${filename}...`);

  setTimeout(() => {
    const content = `這是 ${filename} 的內容`;
    callback(null, content);  // 第一個參數是錯誤(如果沒錯誤則為 null)
  }, 1000);
}

readFile("data.txt", (error, content) => {
  if (error) {
    console.log("發生錯誤:", error);
  } else {
    console.log("檔案內容:", content);
  }
});

// 錯誤處理範例
function divide(a, b, callback) {
  setTimeout(() => {
    if (b === 0) {
      callback(new Error("除數不能為 0"));
    } else {
      callback(null, a / b);
    }
  }, 500);
}

divide(10, 2, (error, result) => {
  if (error) {
    console.log("錯誤:", error.message);
  } else {
    console.log("結果:", result);  // 5
  }
});

回呼地獄(Callback Hell)

多個非同步操作串接時的問題。

// 糟糕的巢狀結構
getUser(userId, (error, user) => {
  if (error) {
    console.log(error);
  } else {
    getPosts(user.id, (error, posts) => {
      if (error) {
        console.log(error);
      } else {
        getComments(posts[0].id, (error, comments) => {
          if (error) {
            console.log(error);
          } else {
            console.log(comments);
            // 繼續巢狀下去...
          }
        });
      }
    });
  }
});

// 這種結構又稱為「金字塔厄運」(Pyramid of Doom)
// 難以閱讀、難以維護、難以除錯

Promise

Promise 是處理非同步的現代方式,解決了回呼地獄問題。

基本概念

// Promise 有三種狀態:
// - pending(進行中)
// - fulfilled(成功)
// - rejected(失敗)

// 建立 Promise
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;

    if (success) {
      resolve("操作成功");  // 轉為 fulfilled 狀態
    } else {
      reject("操作失敗");   // 轉為 rejected 狀態
    }
  }, 1000);
});

// 使用 Promise
promise
  .then(result => {
    console.log(result);  // "操作成功"
  })
  .catch(error => {
    console.log(error);
  });

Promise 鏈

// 將回呼地獄改寫為 Promise 鏈
function getUser(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ id: userId, name: "Alice" });
    }, 500);
  });
}

function getPosts(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { id: 1, title: "文章 1", userId },
        { id: 2, title: "文章 2", userId }
      ]);
    }, 500);
  });
}

function getComments(postId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { id: 1, text: "評論 1", postId },
        { id: 2, text: "評論 2", postId }
      ]);
    }, 500);
  });
}

// 優雅的鏈式呼叫
getUser(1)
  .then(user => {
    console.log("使用者:", user);
    return getPosts(user.id);  // 回傳 Promise
  })
  .then(posts => {
    console.log("文章:", posts);
    return getComments(posts[0].id);
  })
  .then(comments => {
    console.log("評論:", comments);
  })
  .catch(error => {
    console.log("發生錯誤:", error);
  })
  .finally(() => {
    console.log("無論成功或失敗都會執行");
  });

Promise 方法

// Promise.resolve():建立已成功的 Promise
const resolved = Promise.resolve("立即成功");
resolved.then(value => console.log(value));  // "立即成功"

// Promise.reject():建立已失敗的 Promise
const rejected = Promise.reject("立即失敗");
rejected.catch(error => console.log(error));  // "立即失敗"

// Promise.all():等待所有 Promise 完成
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);

Promise.all([p1, p2, p3])
  .then(results => {
    console.log(results);  // [1, 2, 3]
  });

// 只要有一個失敗就整體失敗
const p4 = Promise.resolve(1);
const p5 = Promise.reject("失敗");
const p6 = Promise.resolve(3);

Promise.all([p4, p5, p6])
  .then(results => {
    console.log(results);
  })
  .catch(error => {
    console.log(error);  // "失敗"
  });

// Promise.race():回傳最快完成的那個
const slow = new Promise(resolve => setTimeout(() => resolve("慢"), 2000));
const fast = new Promise(resolve => setTimeout(() => resolve("快"), 500));

Promise.race([slow, fast])
  .then(result => {
    console.log(result);  // "快"
  });

// Promise.allSettled():等待所有 Promise 完成(不管成功或失敗)
const p7 = Promise.resolve(1);
const p8 = Promise.reject("錯誤");
const p9 = Promise.resolve(3);

Promise.allSettled([p7, p8, p9])
  .then(results => {
    console.log(results);
    // [
    //   { status: "fulfilled", value: 1 },
    //   { status: "rejected", reason: "錯誤" },
    //   { status: "fulfilled", value: 3 }
    // ]
  });

// Promise.any():回傳第一個成功的
const p10 = Promise.reject("錯誤 1");
const p11 = Promise.resolve("成功");
const p12 = Promise.reject("錯誤 2");

Promise.any([p10, p11, p12])
  .then(result => {
    console.log(result);  // "成功"
  });

async/await

更現代、更易讀的非同步語法。

基本用法

// async 函式總是回傳 Promise
async function sayHello() {
  return "Hello";
}

sayHello().then(message => console.log(message));  // "Hello"

// 等同於:
function sayHello() {
  return Promise.resolve("Hello");
}

// await 等待 Promise 完成
async function fetchData() {
  console.log("開始取得資料");

  // await 會暫停函式執行,直到 Promise 完成
  const result = await new Promise(resolve => {
    setTimeout(() => resolve("資料"), 2000);
  });

  console.log(result);  // 2 秒後:資料
  return result;
}

fetchData();

// 沒有 async/await 的版本(比較)
function fetchData() {
  console.log("開始取得資料");

  return new Promise(resolve => {
    setTimeout(() => resolve("資料"), 2000);
  }).then(result => {
    console.log(result);
    return result;
  });
}

錯誤處理

// 使用 try-catch
async function fetchUser(id) {
  try {
    const response = await fetch(`https://api.example.com/users/${id}`);

    if (!response.ok) {
      throw new Error(`HTTP 錯誤:${response.status}`);
    }

    const user = await response.json();
    return user;
  } catch (error) {
    console.log("發生錯誤:", error.message);
    return null;
  }
}

// 另一種錯誤處理方式
async function fetchUserAlt(id) {
  const response = await fetch(`https://api.example.com/users/${id}`)
    .catch(error => {
      console.log("網路錯誤:", error);
      return null;
    });

  if (!response) return null;

  const user = await response.json();
  return user;
}

平行執行

// 錯誤:串行執行(慢)
async function getDataSerial() {
  const user = await getUser(1);      // 等待 500ms
  const posts = await getPosts(1);    // 等待 500ms
  const comments = await getComments(1);  // 等待 500ms
  // 總共:1500ms

  return { user, posts, comments };
}

// 正確:平行執行(快)
async function getDataParallel() {
  // 同時發起三個請求
  const [user, posts, comments] = await Promise.all([
    getUser(1),
    getPosts(1),
    getComments(1)
  ]);
  // 總共:500ms(取最慢的那個)

  return { user, posts, comments };
}

// 有依賴關係時
async function getDataWithDependency() {
  // 必須先取得 user
  const user = await getUser(1);

  // 然後平行取得 posts 和 comments
  const [posts, comments] = await Promise.all([
    getPosts(user.id),
    getComments(user.id)
  ]);

  return { user, posts, comments };
}

迴圈中的 async/await

const userIds = [1, 2, 3, 4, 5];

// 串行處理(一個一個來)
async function processSerial() {
  const results = [];

  for (const id of userIds) {
    const user = await getUser(id);  // 等待每個完成
    results.push(user);
  }

  return results;
}

// 平行處理(全部一起)
async function processParallel() {
  const promises = userIds.map(id => getUser(id));
  const results = await Promise.all(promises);
  return results;
}

// 限制並行數量
async function processWithLimit(limit) {
  const results = [];
  const executing = [];

  for (const id of userIds) {
    const promise = getUser(id).then(user => {
      executing.splice(executing.indexOf(promise), 1);
      return user;
    });

    results.push(promise);
    executing.push(promise);

    if (executing.length >= limit) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}

實戰範例

1. 資料載入器

class DataLoader {
  constructor() {
    this.cache = new Map();
  }

  async load(url) {
    // 檢查快取
    if (this.cache.has(url)) {
      console.log("從快取載入");
      return this.cache.get(url);
    }

    try {
      console.log(`載入 ${url}...`);
      const response = await fetch(url);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      const data = await response.json();
      this.cache.set(url, data);
      return data;
    } catch (error) {
      console.error(`載入失敗:${error.message}`);
      throw error;
    }
  }

  clearCache() {
    this.cache.clear();
  }
}

const loader = new DataLoader();
// await loader.load("https://api.example.com/data");

2. 重試機制

async function retry(fn, maxAttempts = 3, delay = 1000) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxAttempts) {
        throw error;
      }

      console.log(`嘗試 ${attempt} 失敗,${delay}ms 後重試...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

// 使用範例
async function unstableApi() {
  if (Math.random() < 0.7) {
    throw new Error("API 錯誤");
  }
  return "成功";
}

retry(() => unstableApi(), 5, 500)
  .then(result => console.log(result))
  .catch(error => console.log("全部失敗:", error.message));

3. 超時控制

function timeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) => {
      setTimeout(() => reject(new Error("超時")), ms);
    })
  ]);
}

// 使用範例
async function slowOperation() {
  await new Promise(resolve => setTimeout(resolve, 5000));
  return "完成";
}

timeout(slowOperation(), 2000)
  .then(result => console.log(result))
  .catch(error => console.log(error.message));  // "超時"

// 包裝成可重用的函式
function withTimeout(fn, ms) {
  return async function(...args) {
    return timeout(fn(...args), ms);
  };
}

const fastOperation = withTimeout(slowOperation, 2000);

4. 批次處理

async function batchProcess(items, batchSize, processor) {
  const results = [];

  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    console.log(`處理批次 ${Math.floor(i / batchSize) + 1}...`);

    const batchResults = await Promise.all(
      batch.map(item => processor(item))
    );

    results.push(...batchResults);
  }

  return results;
}

// 使用範例
const numbers = Array.from({ length: 20 }, (_, i) => i + 1);

async function processNumber(n) {
  await new Promise(resolve => setTimeout(resolve, 100));
  return n * 2;
}

// 每次處理 5 個
batchProcess(numbers, 5, processNumber)
  .then(results => console.log(results));

5. 佇列系統

class AsyncQueue {
  constructor() {
    this.queue = [];
    this.processing = false;
  }

  async add(task) {
    this.queue.push(task);

    if (!this.processing) {
      await this.process();
    }
  }

  async process() {
    this.processing = true;

    while (this.queue.length > 0) {
      const task = this.queue.shift();

      try {
        await task();
      } catch (error) {
        console.error("任務失敗:", error);
      }
    }

    this.processing = false;
  }
}

const queue = new AsyncQueue();

queue.add(async () => {
  console.log("任務 1 開始");
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log("任務 1 完成");
});

queue.add(async () => {
  console.log("任務 2 開始");
  await new Promise(resolve => setTimeout(resolve, 500));
  console.log("任務 2 完成");
});

常見錯誤

1. 忘記 await

// 錯誤
async function bad() {
  const result = getUser(1);  // 忘記 await
  console.log(result);  // Promise 物件,不是資料
}

// 正確
async function good() {
  const result = await getUser(1);
  console.log(result);  // 實際資料
}

2. 在非 async 函式中使用 await

// 錯誤
function bad() {
  const result = await getUser(1);  // 語法錯誤
}

// 正確
async function good() {
  const result = await getUser(1);
}

// 或使用 IIFE
(async () => {
  const result = await getUser(1);
})();

3. 沒有處理錯誤

// 危險
async function risky() {
  const data = await fetchData();  // 如果失敗會拋出錯誤
  return data;
}

// 安全
async function safe() {
  try {
    const data = await fetchData();
    return data;
  } catch (error) {
    console.error("錯誤:", error);
    return null;
  }
}

// 或在呼叫時處理
risky().catch(error => console.error(error));

小結

這篇我們學習了:

  • 同步與非同步的區別
  • 回呼函式與回呼地獄
  • Promise 的使用與方法
  • async/await 語法
  • 錯誤處理與最佳實踐
  • 實戰應用範例

下一步:

完成這篇後,你已經能夠: - 處理非同步操作 - 避免回呼地獄 - 撰寫現代化的異步程式碼

前往下一篇:單元十:模組系統

在那裡,我們會學習如何組織和重用程式碼。


0 留言

目前沒有留言

發表留言
回覆