Featured image of post JavaScript 异步/等待应避免的模式Featured image of post JavaScript 异步/等待应避免的模式

JavaScript 异步/等待应避免的模式

探索 JavaScript 中常见的异步/等待反模式,并了解如何优化异步执行以获得更好的性能和错误处理。

介绍

自从 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 };
}

在这里,fetchPostsfetchUser 完成之前不会开始,并且 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 构建的。请记住这些规则:

  1. 使用 Promise.all 组合独立任务。
  2. 切勿将 forEach 与异步回调一起使用;使用 for...ofPromise.all 代替。
  3. 避免嵌套 try-catch 块,以保持代码可读且易于调试。