JavaScript 开发人员的 Async 和 Await 实用指南
介绍
Async 和 Await 是 ECMAScript 2017 (ES8) 中引入的 JavaScript 关键字,可让您以更易读、更同步且更易于管理的方式编写异步代码。它们简化了需要时间才能完成的处理操作,例如从 API 获取数据。
在深入研究之前,让我们首先了解 JavaScript 中的同步和异步编程的概念。在同步编程中,任务按出现的顺序依次执行。每个任务必须在下一个任务开始之前完成。另一方面,异步编程允许任务在后台运行,使 JavaScript 能够继续执行其他任务而无需等待前一个任务完成。
我们知道,JavaScript 是一种单线程语言,这意味着它一次只能执行一项任务。如果是这样的话,JavaScript 如何处理异步代码呢?这是通过事件循环实现的,事件循环是与 JavaScript 运行时环境一起工作的关键机制。事件循环使异步操作能够在不阻塞主线程的情况下运行,从而确保 JavaScript 保持响应。事不宜迟,拿一杯咖啡,让我们直接进入今天的主题!
什么是异步?
为了更好地理解这个概念,我们将采取一种更实际的方法。在引入 async 和 await 之前,Promises 是使用 ES6(ECMAScript 2015)中引入的“旧方法”处理的。让我们探索下面的例子。

上面的代码演示了处理 Promise 的传统语法。`Promise` 构造函数用于创建一个新的 Promise 实例。它接受一个函数(称为执行器函数)作为其参数,该函数包括两个参数:`resolve` 和 `reject`。此执行器函数包含异步操作的逻辑。在此示例中,`resolve` 被立即调用,表示 Promise 已成功完成并返回特定值。一旦 Promise 被解析,就会触发 `.then` 方法,执行其回调以记录结果。
但是,这种语法可能有点难记。`async/await` 的引入简化了处理承诺的过程,使其更易于阅读和理解。让我们看下面的一个例子。

要实现“async”函数,我们使用“async”关键字,它告诉 JavaScript 这不是常规函数,而是异步函数。第二个示例展示了如何使用箭头函数完成相同的操作。
另一个需要注意的重要概念是 `await` 关键字。`async` 和 `await` 可以协同工作,简化 Promises 的处理。`await` 关键字只能在异步函数内部使用,不能在函数外部或常规函数内部使用。它必须始终出现在标记为 async 的函数内。现在我们已经介绍了基础知识,让我们更深入地了解这个概念!
幕后运作方式
许多 JavaScript 开发人员经常在代码中使用“async/await”,但只有少数人真正了解其底层工作原理。这就是本教程的目的所在。让我们通过一个示例来分解它。

在此示例中,我们使用 `.then` 方法来更好地理解 Promises 与 `async/await` 方法相比的工作原理。当调用 `handlePromise()` 函数时,代码逐行执行。当 JavaScript 遇到 `.then` 方法时,它会将回调注册到微任务队列并立即移至下一行,打印“hello world”。
所有同步任务完成后,JavaScript 引擎会检查微任务队列中是否有待处理的任务。五秒钟后,“setTimeout”完成,其回调被推回到调用堆栈。此时,Promise 已解析,注册的回调运行,并记录结果。
简而言之,JavaScript 引擎不会等待,而是直接转到下一行代码。现在,使用 `async/await` 时是否适用相同的行为,还是会有所不同?让我们来一探究竟!

在上面的例子中,当调用 `handlePromise()` 函数时,会打印第一行“the start”。然后 JavaScript 遇到 `await` 关键字,这表明该函数是异步的并且涉及一个 Promise。它确定由于 `setTimeout`,Promise 将需要五秒钟才能解析。此时,`handlePromise()` 函数被暂停(从调用堆栈中移除),并且函数内部 await 之后的任何代码都将暂停。
JavaScript 引擎继续执行程序的剩余部分。五秒钟后,Promise 被解析,挂起的函数返回到调用堆栈,并按顺序执行 `handlePromise()` 中的剩余行“Promise 被解析”和“结束”。
需要注意的是,暂停函数不会阻塞主线程。如果在 `handlePromise()` 函数之外写了其他代码,它将在 Promise 等待解析时执行。
下面的例子演示了这种行为的实际应用:

在此示例中,第一个输出是“the start”。当 JavaScript 遇到 await 关键字时,它会识别出 Promise 将需要五秒钟才能解析。此时,该函数被暂停,JavaScript 继续执行函数外部的任何代码。因此,接下来会打印“We are outside”。
五秒后,一旦 Promise 被解析,`handlePromise()` 函数就会恢复到调用堆栈,并执行其剩余行,打印 `Promise is solved` 然后是 `the end`。
让我们再看一个例子,我们将尝试使用“async/await”进行 API 调用,以更好地理解这个概念。

在上面的代码中,执行过程遵循前面讨论的相同原则。当 JavaScript 遇到 `fetch` 函数时,它会暂停 `getData()` 函数并等待 fetch 调用返回响应对象,该对象包含各种属性,例如响应的状态、标头和正文。一旦响应可用,该函数就会恢复执行。
响应主体是我们需要的数据,但它是原始格式(例如文本或二进制),不能立即使用。为了将其转换为 JavaScript 对象以便于操作,我们使用 `.json()` 方法,该方法解析原始 JSON 响应。此过程涉及另一个 Promise,这就是为什么第二个 `await` 是必要的。该函数再次挂起,直到 Promise 解析。
一旦两个 Promise 都满足了,`getData()` 函数就会恢复,解析后的数据会打印到控制台。这是一种解释 fetch 工作原理的简单方法,不是吗?现在,回到我们的主要讨论!如果我们的 API 响应失败怎么办?我们如何使用 `async/await` 管理错误?让我们在下一节中深入探讨这个问题。
使用 Async/Await 处理错误
传统上,Promises 中的错误是使用 `.catch` 方法处理的。但是,在使用 async/await 时,我们该如何处理错误?这就是 `try...catch` 块发挥作用的地方。

在上面的代码中,Promise 被封装在 `try` 块中,如果 Promise 解析成功,该块就会执行。但是,如果 Promise 被拒绝,则会在 `catch` 块中捕获并处理错误。
但你知道我们仍然可以用传统方式处理错误吗?以下是一个例子:

要使用传统方法处理 async/await 中的错误,只需将“catch”方法附加到函数,如上所示。它的功能与 try/catch 块相同。
结论
Async/await 彻底改变了 JavaScript 处理异步操作的方式,与 `.then` 和 `.catch` 等传统方法相比,它使代码更具可读性且更易于管理。通过利用 async 和 await,我们可以编写感觉更同步的异步代码,从而提高整体代码清晰度。在了解内部工作原理(例如事件循环和微任务队列)的同时,实现 async/await 对于现代 JavaScript 开发来说既简单又高效。通过使用 `try/catch` 或 `.catch` 进行适当的错误处理,我们可以自信地管理成功和失败的承诺。
感谢您的关注!希望本文能让您更清楚地了解 async/await。祝您在编码冒险中取得成功 — 去创造一些令人惊叹的东西吧!