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 留言
發表留言