介紹
自從 ES2017 中引入 async/await 以來,編寫非同步 JavaScript 變得更加直觀。它允許開發人員以看起來同步的結構表達非同步邏輯,與嵌套的 Promise 鏈 (.then().catch()) 相比,大大提高了程式碼庫的可讀性。
然而,這種乾淨的類似同步的語法可以隱藏效能缺陷和錯誤處理錯誤。 在本文中,我們將深入研究常見的非同步/等待反模式,並學習如何將它們重構為高效能、有彈性的程式碼。
反模式 1:不必要的序列化(順序等待)
最常見的錯誤是順序執行獨立的非同步任務,錯過了並行執行的機會。
應避免的程式碼
// Fetching three independent datasets sequentially
async function getUserProfile(userId) {
const user = await fetchUser(userId); // takes 1.0s
const posts = await fetchPosts(userId); // takes 1.5s
const followers = await fetchFollowers(userId); // takes 0.8s
return { user, posts, followers };
}
在這裡,fetchPosts 在 fetchUser 完成之前不會開始,並且 fetchFollowers 被兩者阻止。總執行時間為 1.0 + 1.5 + 0.8 = 3.3 seconds。由於這些請求彼此不依賴,因此應該同時請求它們。
重構程式碼(使用 Promise.all)
async function getUserProfile(userId) {
// Fire requests concurrently and wait for all to resolve
const [user, posts, followers] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchFollowers(userId)
]);
return { user, posts, followers };
}
使用 Promise.all 會同時觸發所有三個提取。總等待時間降至最長請求的持續時間:1.5 秒。
反模式 2:嵌套的 try-catch 區塊
在處理潛在的 API 故障時,一些開發人員會恢復到非同步函數內的深度嵌套模式,模仿傳統的回調地獄。
應避免的程式碼
async function processData() {
try {
const rawData = await fetchData();
try {
const parsedData = await parseData(rawData);
try {
await saveToDatabase(parsedData);
} catch (dbError) {
console.error("Database save failed", dbError);
}
} catch (parseError) {
console.error("Parsing failed", parseError);
}
} catch (fetchError) {
console.error("Fetch failed", fetchError);
}
}
深度嵌套違背了 async/await 的目的,使得函數難以掃描和維護。
重構程式碼
如果您要擷取管道期間的任何錯誤,請將整個流程包裝在單一頂層 try-catch 區塊中。如果您必須唯一地處理特定錯誤,請將它們在 catch 區塊中分類或將 .catch() 處理程序附加到各個 Promise。
async function processData() {
try {
const rawData = await fetchData();
const parsedData = await parseData(rawData);
await saveToDatabase(parsedData);
} catch (error) {
// Handle or categorize errors centrally
handleProcessingError(error);
}
}
反模式 3:在 Array.prototype.forEach() 內使用非同步回呼
將 forEach 與非同步回呼一起使用將在編譯時沒有語法錯誤,但非同步任務將無序運行。
應避免的程式碼
// Iterating and sending emails asynchronously
async function sendEmails(users) {
users.forEach(async (user) => {
await mailer.send(user.email);
});
console.log("All emails queued or sent!"); // <- Executes before mails actually finish sending!
}
Array.prototype.forEach 不等待其回呼函數傳回的承諾的解決。它同步觸發所有回調。因此,後續日誌語句會在任何電子郵件實際到達收件者之前立即列印。
重構程式碼(使用 for…of 或 Promise.all)
如果任務必須依序執行(一項一項):
async function sendEmailsSerially(users) {
for (const user of users) {
await mailer.send(user.email); // awaits each send
}
console.log("All emails sent successfully.");
}
如果任務應該同時運行:
async function sendEmailsInParallel(users) {
const promises = users.map(user => mailer.send(user.email));
await Promise.all(promises); // waits for all promises to resolve
console.log("All emails sent successfully.");
}
反模式 4:不含await 關鍵字的非同步函數
當函數僅包含同步操作時將其宣告為 async 會造成不必要的效能消耗。
應避免的程式碼
// Simple synchronous math calculation
async function calculateSum(a, b) {
return a + b;
}
當您為函數新增前綴 async 時,執行時間引擎會自動將回傳值包裝在已解析的 Promise 中。這會增加少量的運行時開銷(微任務佇列調度和記憶體分配)。如果函數內部沒有 await ,則保持同步。
結論
Async/await 為編寫非同步 JavaScript 提供了優雅的語法,但它仍然是基於 Promises 建構的。請記住這些規則:
- **使用
Promise.all组合独立任务。 ** - **切勿將
forEach與非同步回呼一起使用;使用for...of或Promise.all取代。 ** - **避免巢狀
try-catch區塊,以保持程式碼可讀且易於偵錯。 **

