JavaScript async/await 異步函式
async/await 是 ES2017 (ES8) 引入的語法,讓你可以用「看起來像同步」的方式撰寫異步程式碼,大幅提升可讀性。它是建立在 Promise 之上的語法糖。
async 函式
在函式前面加上 async 關鍵字,這個函式就變成一個異步函式。async 函式永遠會回傳一個 Promise:
async function hello() {
return 'Hello';
}
// async 函式回傳 Promise
hello().then(function(result) {
console.log(result); // Hello
});
// 等同於
function hello() {
return Promise.resolve('Hello');
}
如果在 async 函式中 throw 錯誤,Promise 會變成 rejected 狀態:
async function fail() {
throw new Error('出錯了');
}
fail().catch(function(err) {
console.log(err.message); // 出錯了
});
await 關鍵字
await 只能在 async 函式內部使用。它會「等待」一個 Promise 完成,並取得 Promise 的結果值:
function delay(ms) {
return new Promise(function(resolve) {
setTimeout(resolve, ms);
});
}
async function demo() {
console.log('開始');
await delay(2000); // 等待 2 秒
console.log('2 秒後');
}
demo();
await 的好處是讓異步程式碼看起來像同步執行,不用再用 .then() 串接:
// 使用 Promise .then()
function fetchData() {
return fetch('/api/user')
.then(function(response) {
return response.json();
})
.then(function(data) {
console.log(data);
});
}
// 使用 async/await - 更直觀
async function fetchData() {
var response = await fetch('/api/user');
var data = await response.json();
console.log(data);
}
錯誤處理
使用 try/catch 來處理 async/await 的錯誤:
async function fetchUser() {
try {
var response = await fetch('/api/user');
if (!response.ok) {
throw new Error('請求失敗');
}
var data = await response.json();
return data;
} catch (err) {
console.error('錯誤:' + err.message);
}
}
你也可以在呼叫 async 函式時用 .catch() 處理錯誤:
async function riskyOperation() {
throw new Error('Something went wrong');
}
riskyOperation().catch(function(err) {
console.error(err.message);
});
多個異步操作
依序執行
如果操作之間有依賴關係,需要依序執行:
async function sequential() {
var user = await fetchUser(1); // 先取得使用者
var posts = await fetchPosts(user.id); // 再取得該使用者的文章
var comments = await fetchComments(posts[0].id); // 最後取得留言
return comments;
}
並行執行
如果操作之間沒有依賴關係,可以用 Promise.all() 並行執行,提升效能:
async function parallel() {
// 錯誤示範:依序執行,總共要等 3 秒
var result1 = await delay(1000);
var result2 = await delay(1000);
var result3 = await delay(1000);
}
async function parallel() {
// 正確做法:並行執行,只要等 1 秒
var [result1, result2, result3] = await Promise.all([
delay(1000),
delay(1000),
delay(1000)
]);
}
實際範例 - 同時取得多個 API 資料:
async function fetchAllData() {
try {
var [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
console.log(users, posts, comments);
} catch (err) {
console.error('其中一個請求失敗:' + err.message);
}
}
在迴圈中使用 await
for 迴圈(依序執行)
async function processItems(items) {
for (var i = 0; i < items.length; i++) {
await processItem(items[i]); // 依序處理每個項目
}
}
forEach 的陷阱
forEach 不會等待 await,這是常見的錯誤:
// 錯誤:forEach 不會等待 await
async function wrong(items) {
items.forEach(async function(item) {
await processItem(item); // 這些會同時開始執行!
});
console.log('完成'); // 會在處理完成前就執行
}
// 正確:使用 for...of
async function correct(items) {
for (var item of items) {
await processItem(item);
}
console.log('完成'); // 所有項目處理完才會執行
}
並行處理陣列
如果要並行處理陣列中的所有項目:
async function processAllParallel(items) {
await Promise.all(items.map(function(item) {
return processItem(item);
}));
console.log('全部完成');
}
實用範例
帶有重試機制的請求
async function fetchWithRetry(url, retries) {
for (var i = 0; i < retries; i++) {
try {
var response = await fetch(url);
return await response.json();
} catch (err) {
if (i === retries - 1) throw err;
console.log('重試中... (' + (i + 1) + ')');
await delay(1000); // 等待 1 秒後重試
}
}
}
// 使用
var data = await fetchWithRetry('/api/data', 3);
設定逾時
function timeout(ms) {
return new Promise(function(_, reject) {
setTimeout(function() {
reject(new Error('請求逾時'));
}, ms);
});
}
async function fetchWithTimeout(url, ms) {
return Promise.race([
fetch(url),
timeout(ms)
]);
}
// 5 秒內沒回應就會拋出錯誤
var response = await fetchWithTimeout('/api/data', 5000);
注意事項
- await 只能在 async 函式內使用
// 錯誤
function test() {
var data = await fetch('/api'); // SyntaxError
}
// 正確
async function test() {
var data = await fetch('/api');
}
- Top-level await(ES2022)
在模組的最外層可以直接使用 await:
// 在 ES Module 中
var response = await fetch('/api/config');
var config = await response.json();
export default config;
- async/await 和 Promise 可以混用
async function getData() {
return 'data';
}
// 可以用 .then()
getData().then(console.log);
// 也可以用 await
var data = await getData();