JavaScript async/await初学者指南 JavaScript 中的async
andawait
关键字提供了一种现代语法来帮助我们处理异步操作。在本教程中,我们将深入了解如何async/await
在我们的 JavaScript 程序中使用来掌握流控制。
在 JavaScript 中,一些操作是异步的。这意味着它们产生的结果或价值不会立即可用。
考虑以下代码:
function fetchDataFromApi() { // Data fetching logic here console.log(data); } fetchDataFromApi(); console.log('Finished fetching data');
avaScript 解释器不会等待异步fetchDataFromApi
函数完成才继续执行下一条语句。因此,它会Finished fetching data
在记录从 API 返回的实际数据之前进行记录。
在许多情况下,这不是期望的行为。幸运的是,我们可以使用async
andawait
关键字让我们的程序在继续之前等待异步操作完成。
此功能在ES2017中被引入 JavaScript,并在所有现代浏览器中得到支持。
如何创建 JavaScript 异步函数
让我们仔细看看fetchDataFromApi
函数中的数据获取逻辑。JavaScript 中的数据获取是异步操作的一个主要示例。
使用Fetch API,我们可以做这样的事情:
function fetchDataFromApi() { fetch('https://v2.jokeapi.dev/joke/Programming?type=single') .then(res => res.json()) .then(json => console.log(json.joke)); } fetchDataFromApi(); console.log('Finished fetching data');
在这里,我们从JokeAPI获取一个编程笑话。API 的响应采用 JSON 格式,因此我们在请求完成后提取该响应(使用该json()
方法),然后将笑话记录到控制台。
请注意,JokeAPI 是第三方 API,因此我们无法保证返回的笑话质量!
如果我们在您的浏览器或 Node(17.5+ 版本使用--experimental-fetch
标志)中运行此代码,我们将看到内容仍然以错误的顺序记录到控制台。
让我们改变它。
异步关键字
我们需要做的第一件事是将包含函数标记为异步。我们可以通过使用async
关键字来做到这一点,我们将其放在关键字前面function
:
async function fetchDataFromApi() { fetch('https://v2.jokeapi.dev/joke/Programming?type=single') .then(res => res.json()) .then(json => console.log(json.joke)); }
异步函数总是返回一个 promise(稍后会详细介绍),因此已经可以通过将 a 链接then()
到函数调用来获得正确的执行顺序:
fetchDataFromApi() .then(() => { console.log('Finished fetching data'); });
如果我们现在运行代码,我们会看到如下内容:
If Bill Gates had a dime for every time Windows crashed ... Oh wait, he does. Finished fetching data
但我们不想那样做!JavaScript 的 promise 语法可能有点复杂,这就是async/await
闪光点:它使我们能够使用看起来更像同步代码且更具可读性的语法编写异步代码。
等待关键字
接下来要做的是将await
关键字放在我们函数中任何异步操作的前面。这将强制 JavaScript 解释器“暂停”执行并等待结果。我们可以将这些操作的结果分配给变量:
async function fetchDataFromApi() { const res = await fetch('https://v2.jokeapi.dev/joke/Programming?type=single'); const json = await res.json(); console.log(json.joke); }
我们还需要等待调用fetchDataFromApi
函数的结果:
await fetchDataFromApi(); console.log('Finished fetching data');
不幸的是,如果我们现在尝试运行代码,我们会遇到一个错误:
Uncaught SyntaxError: await is only valid in async functions, async generators and modules
这是因为我们不能在非模块脚本中使用函数await
外部。async
我们稍后会更详细地讨论这个问题,但目前解决这个问题最简单的方法是将调用代码包装在它自己的函数中,我们也将其标记为async
:
async function fetchDataFromApi() { const res = await fetch('https://v2.jokeapi.dev/joke/Programming?type=single'); const json = await res.json(); console.log(json.joke); } async function init() { await fetchDataFromApi(); console.log('Finished fetching data'); } init();
如果我们现在运行代码,一切都应该以正确的顺序输出:
UDP is better in the COVID era since it avoids unnecessary handshakes. Finished fetching data
不幸的是,我们需要这个额外的样板文件,但在我看来,代码仍然比基于 promise 的版本更容易阅读。
声明异步函数的不同方式
前面的示例使用了两个命名函数声明(function
关键字后跟函数名称),但我们不限于这些。我们还可以将函数表达式、箭头函数和匿名函数标记为 being async
。
如果您想复习一下函数声明和函数表达式之间的区别,请查看我们关于何时使用 which 的指南。
异步函数表达式
函数表达式是当我们创建一个函数并将其分配给一个变量时。该函数是匿名的,这意味着它没有名称。例如:
const fetchDataFromApi = async function() { const res = await fetch('https://v2.jokeapi.dev/joke/Programming?type=single'); const json = await res.json(); console.log(json.joke); }
这将以与我们之前的代码完全相同的方式工作。
异步箭头函数
ES6 语言中引入了箭头函数。它们是函数表达式的紧凑替代品,并且始终是匿名的。它们的基本语法如下:
(params) => { <function body> }
要将箭头函数标记为异步,请在async
左括号前插入关键字。
例如,init
在上面的代码中创建附加函数的替代方法是将现有代码包装在IIFE中,我们将其标记为async
:
(async () => { async function fetchDataFromApi() { const res = await fetch('https://v2.jokeapi.dev/joke/Programming?type=single'); const json = await res.json(); console.log(json.joke); } await fetchDataFromApi(); console.log('Finished fetching data'); })();
使用函数表达式或函数声明之间没有太大区别:主要只是偏好问题。但是有几件事需要注意,例如提升,或者箭头函数不绑定其自身this
值的事实。您可以查看上面的链接以获取更多详细信息。
JavaScript Await/Async 在幕后使用 Promises
正如您可能已经猜到的那样,async/await
在很大程度上是 promise 的语法糖。让我们更详细地了解一下,因为更好地了解引擎盖下发生的事情将大大有助于理解其async/await
工作原理。
如果您不确定什么是承诺,或者想快速复习一下,请查看我们的承诺指南。
首先要注意的是,一个async
函数总是会返回一个承诺,即使我们没有明确地告诉它这样做。例如:
async function echo(arg) { return arg; } const res = echo(5); console.log(res);
这记录了以下内容:
Promise { <state>: "fulfilled", <value>: 5 }
承诺可以处于三种状态之一:pending、fulfilled或rejected。承诺以未决状态开始生活。如果与 promise 相关的操作成功,则称 promise 已实现。如果操作不成功,则称承诺被拒绝。一旦一个 promise 被 fulfilled 或 rejected,但不是 pending,它也被认为是settled。
当我们在函数await
内部使用关键字async
来“暂停”函数执行时,真正发生的是我们正在等待承诺(显式或隐式)进入已解决或已拒绝状态。
基于我们上面的示例,我们可以执行以下操作:
async function echo(arg) { return arg; } async function getValue() { const res = await echo(5); console.log(res); } getValue(); // 5
因为该echo
函数返回一个承诺,并且函数await
内的关键字getValue
在继续执行该程序之前等待该承诺实现,所以我们能够将所需的值记录到控制台。
Promises 是对 JavaScript 中流控制的重大改进,并被一些较新的浏览器 API 使用,例如Battery status API、Clipboard API、Fetch API、MediaDevices API等等。
Node 还在其内置模块中添加了一个promisify 函数util
,该模块将使用回调函数的代码转换为返回承诺。从 v10 开始,Node 的fs 模块中的函数可以直接返回 promises。
从承诺切换到异步/等待
那么为什么这对我们很重要呢?
嗯,好消息是任何返回承诺的函数都可以与async/await
. 我并不是说我们应该async/await
做所有的事情(这种语法确实有它的缺点,当我们开始错误处理时我们会看到),但我们应该意识到这是可能的。
我们已经了解了如何更改文章顶部的基于 promise 的获取调用以使用async/await
,因此让我们看一下另一个示例。这是一个使用 Node 的基于 promise 的 API 及其readFile
方法获取文件内容的小实用函数。
使用Promise.then()
:
const { promises: fs } = require('fs'); const getFileContents = function(fileName) { return fs.readFile(fileName, enc) } getFileContents('myFile.md', 'utf-8') .then((contents) => { console.log(contents); });
这样就变成了async/await
:
import { readFile } from 'node:fs/promises'; const getFileContents = function(fileName, enc) { return readFile(fileName, enc) } const contents = await getFileContents('myFile.md', 'utf-8'); console.log(contents);
注意:这是利用了一个叫做顶级 await的特性,它只在 ES 模块中可用。要运行此代码,请将文件另存为index.mjs
并使用 Node >= 14.8 的版本。
虽然这些都是简单的例子,但我发现async/await
语法更容易理解。在处理多个then()
语句和混合处理错误时尤其如此。我不会将现有的基于 promise 的代码转换为使用async/await
,但如果您对此感兴趣,VS Code 可以为您完成。
JavaScript async/await初学者指南 推荐阅读
Spring Boot教程10 – Spring Boot应用程序属性