你必須知道的 JavaScript async/await 模式
异步程式設計是現代 JavaScript 的核心部分,而 async/await 讓它變得更加可讀。但許多開發者仍然會遇到常見的陷阱:未處理的 promise 拒絕、緩慢的循序請求,或是令人困惑的錯誤處理。在本文中,我們將探討實用的 async/await 模式,你可以立即應用這些模式來寫出更簡潔、更有效率的程式碼。
1. 基本的 async/await 語法
async 函式總是會回傳一個 Promise。await 關鍵字會暫停執行,直到 Promise 敲定,然後回傳其解析值。如果 Promise 被拒絕,它會拋出一個錯誤,你可以用 try/catch 來捕捉。
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('Fetch failed:', error);
}
}
這是基礎。但現實世界的情境通常需要更細緻的方法。
2. 優雅地處理錯誤
async/await 最大的優點之一,就是你可以用 try/catch 同時處理同步與异步錯誤。然而,在單一 try 區塊中要小心多個 await 呼叫:如果第一個失敗,後續的就不會執行。
為了更好的粒度,你可以將每個 await 包在各自的 try/catch 中,或使用輔助函式來避免重複的 try/catch 區塊。
async function getUser(id) {
try {
const user = await fetchUser(id);
return user;
} catch (error) {
console.error('Failed to fetch user:', error);
throw error;
}
}
3. 並行執行 promises
循序地 await 多個獨立的 promises 是常見的效能錯誤。如果你需要來自多個來源的結果,請使用 Promise.all() 來並行執行它們。
// 循序:慢
const user = await fetchUser();
const posts = await fetchPosts();
// 並行:快
const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]);
當你想要等待所有 promises 敲定,無論它們是解析還是拒絕,請使用 Promise.allSettled()。當你想要收集結果,即使有些失敗時,這很有用。
4. 當順序很重要時,循序執行
有時你需要按順序處理項目,例如當每一步都依賴前一步時。在這種情況下,請使用 for...of 迴圈搭配 await,而不是 forEach(它不會等待异步回呼)。
for (const item of items) {
await processItem(item);
}
如果你需要限制並行數量,可以考慮使用像 p-limit 這樣的函式庫,或實作一個簡單的佇列。
5. 避免迴圈中的 async/await 陷阱
在 Array.prototype.forEach 內使用 await 是一個經典錯誤:forEach 不會等待异步回呼,所以迴圈會在 promises 解析之前就結束。請始終使用 for...of 或 map 搭配 Promise.all。
6. 帶有逾時的 async/await
為了防止請求懸置,你可以讓一個 promise 與逾時進行競爭。
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
}
await Promise.race([fetchData(), timeout(5000)]);
這個模式對於可能懸置的 API 呼叫很方便。
7. 异步生成器與 for await...of
异步生成器允許你异步地產生一連串的值。你可以用 for await...of 來消費它們。
async function* fetchPages() {
let page = 1;
while (true) {
const data = await fetch(`/api/items?page=${page}`);
if (data.length === 0) break;
yield data;
page++;
}
}
for await (const page of fetchPages()) {
console.log(page);
}
這對於分頁 API 或串流資料非常有用。
8. 常見錯誤與如何避免
- 忘記 await: 呼叫 async 函式而沒有 await 會回傳一個 Promise,錯誤可能不會被處理。
- 在非 async 函式中使用 await: 語法錯誤。始終將函式標記為 async。
- 過度使用循序 await: 盡可能使用 Promise.all。
- 未處理拒絕: 始終加入 .catch 或 try/catch。
9. 乾淨异步程式碼的最佳實踐
- 為了可讀性,優先使用 async/await 而非原始 Promises。
- 對於獨立操作,使用 Promise.all。
- 在適當的層級處理錯誤。
- 保持 async 函式小而專注。
- 在迴圈中謹慎使用 async/await;選擇正確的模式。
常見問題
async/await 與 Promises 有什麼區別?
async/await 是 Promises 的語法糖,讓异步程式碼看起來像同步。它簡化了鏈式呼叫和用 try/catch 進行錯誤處理。
我可以在 forEach 迴圈內使用 await 嗎?
不行,forEach 不會等待异步回呼。請改用 for...of 或 Promise.all 搭配 map。
如何並行執行多個异步操作?
使用 Promise.all() 來並行執行它們,並等待全部解析。
當處理 JSON API 時,你可以使用我們的 JSON Formatter 快速格式化並驗證回應。