非同步程式設計(Async / Await)#
JavaScript 是單執行緒語言,同一時間只能做一件事。遇到需要等待的操作(網路請求、讀取檔案)時,非同步機制會先把「等待完成後要做的事」登記起來,繼續執行後面的程式碼,等結果回來再執行:
1
2
3
4
5
| fetchDataAsync().then(data => {
console.log("資料回來了"); // 等結果回來才執行
});
console.log("不需要等待,立即執行"); // 這行先執行
// 輸出順序:不需要等待,立即執行 → 資料回來了
|
非同步的三種寫法演進#
JavaScript 處理非同步操作的方式歷經三個階段的演進,每一代都是為了解決上一代的問題:
現代 JavaScript 推薦使用 async/await,搭配 Promise.all 處理平行操作。
Callback(回呼函式)#
Callback 是最早的非同步處理方式:把「完成後要執行的函式」當作參數傳進去,等非同步操作完成後再呼叫它。
setTimeout / setInterval 是最簡單的 callback 範例——傳入的箭頭函式就是 callback,由瀏覽器在指定時間後呼叫:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // setTimeout — 延遲一段時間後執行一次
setTimeout(() => {
console.log("2 秒後執行"); // 這個函式就是 callback
}, 2000);
// setInterval — 每隔一段時間重複執行
let count = 0;
const timer = setInterval(() => {
count++;
console.log(`第 ${count} 次`);
if (count === 3) clearInterval(timer); // 執行 3 次後停止
}, 1000);
// 取消尚未執行的 setTimeout
const t = setTimeout(() => console.log("不會執行"), 5000);
clearTimeout(t);
|
實際應用中,callback 用來處理非同步操作的結果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| function loadData(url, onSuccess, onError) {
setTimeout(() => {
if (url) {
onSuccess({ data: "結果" });
} else {
onError(new Error("URL 不能為空"));
}
}, 1000);
}
loadData(
"https://api.example.com",
data => console.log("成功:", data),
err => console.error("失敗:", err)
);
|
Callback 的問題是,當多個非同步操作有先後依賴關係時,程式碼會不斷往右縮排,形成難以維護的回呼地獄(Callback Hell):
1
2
3
4
5
6
7
8
| // 先取得使用者 → 再取得訂單 → 再取得商品 → ...
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getProducts(orders[0].id, (products) => {
// 越來越深,難以閱讀和維護
});
});
});
|
Promise 和 async/await 的出現就是為了解決這個問題。
Promise#
Promise(承諾)是一個代表「尚未完成的非同步操作」的物件。你可以把它理解成:「我現在給你一張憑證,結果出來之後你再來兌換。」
Promise 有三種狀態,且只會轉換一次,不可逆:
pending(等待中)
├─ resolve() → fulfilled(已成功)
└─ reject() → rejected(已失敗)
建立 Promise:傳入一個執行函式,內部呼叫 resolve(成功)或 reject(失敗):
1
2
3
4
5
6
7
8
9
| const fetchUser = (id) => new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: "Alice" }); // 成功,傳入結果
} else {
reject(new Error("Invalid ID")); // 失敗,傳入錯誤
}
}, 1000);
});
|
使用 Promise:用 .then() 接成功結果,.catch() 接錯誤,.finally() 無論如何都執行:
1
2
3
4
| fetchUser(1)
.then(user => console.log("使用者:", user)) // 成功時執行
.catch(err => console.error("錯誤:", err)) // 失敗時執行
.finally(() => console.log("無論如何都執行")); // 最後執行
|
Promise 鏈式呼叫#
.then() 本身也回傳一個新的 Promise,因此可以串接多個 .then(),取代巢狀 callback:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 依序:取得使用者 → 取得訂單 → 處理訂單
fetchUser(1)
.then(user => {
console.log("取得使用者:", user.name);
return fetchOrders(user.id); // 回傳新的 Promise,下一個 .then 會等它
})
.then(orders => {
console.log("訂單數量:", orders.length);
return processOrders(orders);
})
.then(result => {
console.log("處理完成:", result);
})
.catch(err => {
// 任一步驟失敗,直接跳到這裡
console.error("發生錯誤:", err);
});
|
關鍵:在 .then() 中 return 一個 Promise,下一個 .then() 就會等它完成再執行。
Promise 靜態方法#
當需要同時處理多個 Promise 時,可以使用這四個靜態方法:
| 方法 | 完成條件 | 失敗條件 | 適用情境 |
|---|
Promise.all | 全部成功 | 任一失敗即失敗 | 全部都必須成功 |
Promise.allSettled | 全部結束(不論結果) | 不會失敗 | 需要知道每個結果 |
Promise.race | 第一個完成(不論成敗) | 第一個失敗 | 取最快的結果 |
Promise.any | 第一個成功 | 全部失敗才失敗 | 取第一個成功的 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Promise.all — 同時發出多個請求,全部完成才繼續
const [users, products] = await Promise.all([
fetchUsers(),
fetchProducts()
]);
// Promise.allSettled — 等全部結束,逐一檢查結果
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
results.forEach(r => {
if (r.status === "fulfilled") console.log("成功:", r.value);
else console.error("失敗:", r.reason);
});
// Promise.race — 取第一個完成的(常用於實作 timeout)
const fastest = await Promise.race([fetchFast(), fetchSlow()]);
// Promise.any — 取第一個成功的,全部失敗才算失敗
const first = await Promise.any([mayFail(), maySucceed()]);
|
async / await#
async/await 是 ES2017 引入的語法,讓非同步程式碼讀起來像同步,是目前最主流的寫法。
async 函式:在 function 前加上 async,該函式的回傳值會自動包成 Promise:
1
2
3
4
5
| async function greet() {
return "Hello"; // 等同於 return Promise.resolve("Hello")
}
greet().then(msg => console.log(msg)); // Hello
|
await:只能用在 async 函式內,讓程式暫停等待 Promise 完成,再繼續往下執行。它不會阻塞整個程式,只是暫停當前這個 async 函式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| async function loadUserData(userId) {
try {
const user = await fetchUser(userId); // 等 fetchUser 完成
const orders = await fetchOrders(user.id); // 等 fetchOrders 完成
const summary = await buildSummary(orders);
return summary;
} catch (err) {
// 任一個 await 拋出錯誤都會到這裡
console.error("載入失敗:", err);
throw err;
}
}
// async 函式回傳 Promise,所以可以用 .then/.catch
loadUserData(1)
.then(summary => console.log(summary))
.catch(err => console.error(err));
// 或在另一個 async 函式內繼續用 await
async function main() {
const summary = await loadUserData(1);
console.log(summary);
}
|
並行執行#
await 會等待,所以如果把多個獨立的請求逐一 await,它們會依序執行,浪費時間:
1
2
3
4
5
6
7
8
9
10
11
12
| // 錯誤做法:依序等待(慢)
async function sequential() {
const a = await fetchA(); // 等 1 秒後才發出下一個請求
const b = await fetchB(); // 再等 1 秒
// 共需 2 秒
}
// 正確做法:先同時發出所有請求,再一起等待結果
async function parallel() {
const [a, b] = await Promise.all([fetchA(), fetchB()]);
// fetchA 和 fetchB 同時發出,共只需 1 秒
}
|
原則:沒有先後依賴關係的請求,用 Promise.all 並行發出。
實際範例:載入資料並顯示#
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
| async function displayUser(userId) {
const loading = document.querySelector("#loading");
const content = document.querySelector("#content");
loading.hidden = false;
content.innerHTML = "";
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
// fetch 只有網路錯誤才會 reject,HTTP 4xx/5xx 需手動判斷
if (!response.ok) {
throw new Error(`HTTP 錯誤:${response.status}`);
}
const user = await response.json();
content.innerHTML = `
<h2>${user.name}</h2>
<p>Email: ${user.email}</p>
<p>城市: ${user.address.city}</p>
`;
} catch (err) {
content.innerHTML = `<p style="color:red">載入失敗:${err.message}</p>`;
} finally {
loading.hidden = true; // 無論成功或失敗,都隱藏 loading
}
}
|
Reference#