Async/Await 与 Promises:JavaScript 初学者的简单指南

您是否曾有过在咖啡店排队等待 JavaScript 为您拿拿铁的感觉?异步编程经常会有这种感觉 — 同时处理多个订单可能会让您陷入等待。幸运的是,像 和 这样的工具可确保流程保持顺畅和高效,让您的代码继续运行而不会出现延迟。

在本指南中,我们将详细分析 Promises 的工作原理、引入 async/await 的原因以及它如何简化异步代码的编写。无论您是初学者,想要掌握这些概念,还是想弄清楚何时使用每种方法,本文都将帮助您掌握基础知识。

什么是承诺?

Promise 是 JavaScript 中处理异步操作的一个基本概念。Promise 的核心是可能可用的值,或。可以将其视为包裹的追踪号:虽然您还没有收到包裹,但追踪号会让您确信包裹正在运送途中(或让您知道是否出了问题)。

基于“现在、以后或永不”的叙述,Promise 实际上以以下三种状态之一运行:

  • 待定:异步操作尚未完成。
  • 已实现:操作已成功完成,并且 Promise 现在持有结果。
  • 被拒绝:出现错误,并且 Promise 提供了错误。
  • 创建和使用 Promises 需要一个简单的 API。以下是定义 Promise 的方法:

    const fetchData = new Promise((resolve, reject) => {
      setTimeout(() => {
        const data = { id: 1, name: "JavaScript Basics" };
        resolve(data); // Simulates a successful operation
        // reject("Error: Unable to fetch data"); // Simulates a failure
      }, 1000);
    });

    为了处理结果,你可以将 `.then()`、`.catch()` 和 `.finally()` 方法链接到 Promise 对象:

    fetchData
      .then((data) => {
        console.log("Data received:", data);
      })
      .catch((error) => {
        console.error(error);
      })
      .finally(() => {
        console.log("Operation complete.");
      });

    `then()` 方法中的回调在 Promise 解析成功时执行。`.catch()` 方法中的回调在 Promise 解析失败时执行。`finally()` 方法中的回调在 Promise 解析后执行,与解析结果无关。

    Promise 的好处

    Promises 提供了一种比深度嵌套回调更干净的替代方案,深度嵌套回调通常被称为“回调地狱”。Promises 允许链接回调,而不是堆叠回调,从而使操作流程更容易遵循:

    doTask1()
      .then((result1) => doTask2(result1))
      .then((result2) => doTask3(result2))
      .catch((error) => console.error("An error occurred:", error));

    如果使用传统回调编写,则相同的代码将如下所示:

    doTask1((error1, result1) => {
      if (error1) {
        console.error("An error occurred:", error1);
        return;
      }
      doTask2(result1, (error2, result2) => {
        if (error2) {
          console.error("An error occurred:", error2);
          return;
        }
        doTask3(result2, (error3, result3) => {
          if (error3) {
            console.error("An error occurred:", error3);
            return;
          }
          console.log("Final result:", result3);
        });
      });
    });

    令人困惑,不是吗?这就是为什么 Promises 在推出时会彻底改变 JavaScript 编码标准。

    Promises 的缺点

    虽然 Promises 极大地改进了传统的回调函数,但它们也存在着独特的挑战。尽管它们有很多好处,但在复杂的场景中,它们可能会变得难以驾驭,导致代码冗长和调试困难。

    即使使用 `.then()` 链式调用,Promises 在处理多个异步操作时也会导致代码混乱。例如,使用 `.then()` 块管理顺序操作和使用 `.catch()` 处理错误会让人感觉重复且难以理解。

    doTask1()
      .then((result1) => doTask2(result1))
      .catch(Task1Error, (error) => console.error("An error occurred in task 1:", error));
    
      .then((result2) => doTask3(result2))
      .catch(Task2Error, (error) => console.error("An error occurred in task 2:", error));
    
      .catch((error) => console.error("An error occurred in the chain:", error));

    虽然比嵌套回调更简洁,但链式语法仍然很冗长,尤其是在需要详细的自定义错误处理逻辑时。此外,忘记在链式调用的末尾添加 `.catch()` 可能会导致无提示失败,从而使调试变得棘手。

    此外,Promises 中的堆栈跟踪不如同步代码中的堆栈跟踪直观。当发生错误时,堆栈跟踪可能无法清楚地指示问题在异步流程中的来源。

    最后,尽管 Promises 有助于减少回调地狱,但当任务相互依赖时,它们仍然会导致复杂性。嵌套的 `.then()` 块可能会在某些用例中再次出现,从而带来一些它们原本要解决的可读性挑战。

    进入 async/await

    随着 ES2017 (ES8) 中引入 async/await,JavaScript 中的异步编程向前迈进了一大步。async/await 建立在 Promises 之上,允许开发人员编写外观和行为更像同步代码的异步代码。这使其成为提高可读性、简化错误处理和减少冗长程度的真正变革者。

    什么是 async/await?

    Async/await 是一种旨在使异步代码更易于理解和维护的语法。

    async 关键字用于声明始终返回 Promise 的函数。在此函数中,await 关键字会暂停执行,直到 Promise 被解决或拒绝。这样,即使对于复杂的异步操作,流程也会变得线性且直观。

    下面是一个示例,说明 async/await 如何简化上面看到的相同代码示例:

    async function executeTasks() {
      try {
        const result1 = await doTask1();
        const result2 = await doTask2(result1);
        const result3 = await doTask3(result2);
        console.log("All tasks completed successfully:", result3);
      } catch (error) {
        if (error instanceof Task1Error) {
          console.error("An error occurred in task 1:", error);
        } else if (error instanceof Task2Error) {
          console.error("An error occurred in task 2:", error);
        } else {
          console.error("An error occurred in the chain:", error);
        }
      }
    }
    
    executeTasks();

    Async/await 消除了对 `.then()` 链的需求,允许代码按顺序流动。这使得遵循逻辑变得更容易,特别是对于需要一个接一个执行的任务。

    使用 Promises 时,必须使用 `.catch()` 在链的每个级别捕获错误。另一方面,Async/await 使用 try/catch 整合错误处理,减少重复并提高清晰度。

    Async/await 比 Promises 产生更直观的堆栈跟踪。当发生错误时,跟踪会反映实际的函数调用层次结构,从而减少调试的麻烦。总体而言,async/await 感觉更“自然”,因为它与同步代码的编写方式一致。

    比较 Promises 和 async/await

    正如您已经看到的,Async/await 在可读性方面表现出色,尤其是对于顺序操作。 Promises 及其 `.then()` 和 `.catch()` 链式结构很快就会变得冗长或复杂。相比之下,async/await 代码更容易理解,因为它模仿了同步结构。

    灵活性

    Promises 仍然有其用武之地,尤其是对于并发任务。`Promise.all()` 和 `Promise.race()` 等方法对于并行运行多个异步操作更有效。Async/await 也可以处理这种情况,但它需要额外的逻辑才能实现相同的结果。

    // A Promise.all() example
    Promise.all([task1(), task2(), task3()])
      .then((results) => console.log("All tasks completed:", results))
      .catch((error) => console.error("An error occurred:", error));

    错误处理

    虽然使用单个 `.catch()` 进行集中式错误处理对于 Promises 的线性链非常有效,但为了获得最佳可读性,建议对链中不同的错误类型使用分布式 `.catch` 调用。

    另一方面,try/catch 块提供了更自然的结构来处理错误,尤其是在处理连续任务时。

    表现

    在性能方面,async/await 本质上等同于 Promises,因为它建立在 Promises 之上。但是,对于需要并发的任务,“Promise.all()” 可能更高效,因为它允许多个 Promises 并行执行,如果任何一个 Promise 拒绝,则会快速失败。

    何时使用哪个

    如果您的任务涉及大量并发操作,例如同时从多个 API 获取数据,那么 Promises 很可能是更好的选择。如果您的异步代码不涉及大量链接,那么 Promises 也非常适合这种情况,因为它很简单。

    另一方面,async/await 在需要按顺序执行大量任务或优先考虑可读性和可维护性的情况下表现出色。例如,如果您有一系列依赖操作,例如获取数据、转换数据和保存数据,async/await 提供了一个干净且同步的结构。这使得遵循操作流程变得更容易,并简化了使用 try/catch 块的集中错误处理。对于初学者或优先考虑可读代码的团队来说,async/await 特别有用。

    结论

    JavaScript 提供了两种强大的工具来管理异步操作:Promises 和 async/await。Promises 彻底改变了开发人员处理异步任务的方式,解决了回调地狱等问题并实现了链式调用。Async/await 以 Promises 为基础,提供了更简洁的语法,感觉更自然、更直观,尤其是对于顺序任务而言。

    现在您已经探索了这两种方法,您可以选择最适合您需求的方法。尝试将基于 Promise 的函数转换为 async/await,并观察可读性方面的差异!