介绍
自从 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块,以保持代码可读且易于调试。

