JavaScript中的asyncawait关键字提供了一种现代语法,帮助我们处理异步操作。在本教程中,我们将深入研究如何使用async/await来掌控JavaScript程序中的流程控制。

总览

  • 如何创建JavaScript异步函数

    • async关键字
    • await关键字
    • 声明异步函数的不同方式
  • await/async内部机制
    • 从promise到async/await的转换
  • 错误处理
    • 在函数调用中使用catch()
  • 并行运行异步命令
  • 同步循环中的异步await
  • 顶层await
  • 总结

在JavaScript中,一些操作是异步的。这意味着它们产生的结果或者值不会立即奏效。

看看下面的代码:

function fetchDataFromApi() {
// Data fetching logic here
console.log(data);
} fetchDataFromApi();
console.log('Finished fetching data');

JavaScript解释器不会等待异步fetchDataFromApi函数完成后再解释下一条语句。因此,在打印API返回的真实数据之前,它就会打印Finished fetching data

大多数情况下,这并不是我们想要的行为。幸运的是,我们可以使用asyncawait关键字,使我们的程序在继续前进之前等待异步操作的完成。

这个功能是在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关键字

我们需要做的第一件事是将包含的函数标记为异步的。我们可以通过使用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(后面会详细介绍),所以可以通过在函数调用上链接一个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关键字

接下来要做的是,在我们的函数中的任何异步操作前面加上 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

这是因为我们不能在非模块脚本中的async函数之外使用await。我们将在后面详细讨论这个问题,但现在解决这个问题的最简单的方法是将调用的代码包裹在一个自己的函数中,我们也会将其标记为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关键字后跟着函数名字),但我们并不局限于这些。我们也可以把函数表达式、箭头函数和匿名函数标记为async

异步函数表达式

当我们创建一个函数,并将其赋值给一个变量时,这便是函数表达式。该函数是匿名的,这意味着它没有名字。比如:

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的事实。

Await/Async内部机制

正如你可能已经猜到的,async/await在很大程度上是promise的语法糖。让我们更详细地看一下这个问题,因为更好地理解内部发生的事情将对理解async/await的工作方式有很大帮助。

第一件需要注意的事情是,async函数总是返回一个promise,即使我们不显式地告诉它这么做。比如:

async function echo(arg) {
return arg;
} const res = echo(5);
console.log(res);

打印结果如下:

Promise { <state>: "fulfilled", <value>: 5 }

promise可能会是三种状态之一:pendingfulfilled、或者rejected。一个promise开始时处于pending状态。如果与该promise有关的行为成功了,该promise就被称为fulfilled。如果行为不成功,该promise就被称为rejected。一旦promisefulfilled或者rejected,但不是pending,它也被认为是settled

当我们在async函数中使用 await 关键字来"暂停"函数执行时,真正发生的是我们在等待一个promise(无论是显式还是隐式)进入resolvedrejected状态。

基于上述示例,我们可以这么做:

async function echo(arg) {
return arg;
} async function getValue() {
const res = await echo(5);
console.log(res);
} getValue();
// 5

因为echo函数返回一个promise,而getValue函数中的await关键字在继续程序之前等待这个promise完成,所以我们能够将所需的值打印到控制台。

promise是对JavaScript中流程控制的一大改进,并且被一些较新的浏览器API所使用。比如Battery status APIClipboard APIFetch APIMediaDevices API等等。

Node还在其内置的util模块中添加了一个promise函数,可以将使用回调函数的代码转换为返回promise。而从v10开始,Node的fs模块中的函数可以直接返回promise

从promise到async/await的转换

那么,为什么这一切对我们来说都很重要呢?

好消息是,任何返回promise的函数都可以使用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);

注意:这是在利用一个叫做top-level await的功能,它只在ES模块中可用。要运行这段代码,请将文件保存为index.mjs并使用Node>=14.8的版本。

虽然这些都是简单的例子,但我发现async/await的语法更容易理解。当处理多个then()语句和错误处理时,这一点变得尤其真实。

错误处理

在处理异步函数时,有几种方法来处理错误。最常见的可能是使用try...catch块,我们可以把它包在异步操作中并捕捉任何发生的错误。

在下面的例子中,请注意我是如何将URL改成不存在的东西的:

async function fetchDataFromApi() {
try {
const res = await fetch('https://non-existent-url.dev');
const json = await res.json();
console.log(json.joke);
} catch (error) {
// Handle the error here in whichever way you like
console.log('Something went wrong!');
console.warn(error)
}
} await fetchDataFromApi();
console.log('Finished fetching data');

这将导致以下信息被打印到控制台:

Something went wrong!
TypeError: fetch failed
...
cause: Error: getaddrinfo ENOTFOUND non-existent-url.dev
Finished fetching data

这种结果是因为fetch返回一个promise。当fetch操作失败时,promisereject方法被调用,await关键字将这种reject转换为一个可捕捉的错误。

然而,这种方法有几个问题。主要的问题是它很啰嗦,而且相当难看。想象一下,我们正在构建一个CRUD应用程序,我们为每个CRUD方法(创建、读取、更新、销毁)都有一个单独的函数。如果这些方法中的每一个都进行了异步API调用,我们就必须把每个调用包在自己的try...catch块中。这是相当多的额外代码。

另一个问题是,如果我们不使用await关键字,这将导致一个未处理的拒绝的promise

import { readFile } from 'node:fs/promises';

const getFileContents = function(fileName, enc) {
try {
return readFile(fileName, enc)
} catch (error) {
console.log('Something went wrong!');
console.warn(error)
}
} const contents = await getFileContents('this-file-does-not-exist.md', 'utf-8');
console.log(contents);

上述代码的打印如下:

node:internal/process/esm_loader:91
internalBinding('errors').triggerUncaughtException(
^
[Error: ENOENT: no such file or directory, open 'this-file-does-not-exist.md'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: 'this-file-does-not-exist.md'
}

await不同,return关键字不会将拒绝的promise转化为可捕捉的错误。

在函数调用中使用catch()

每个返回promise的函数都可以利用promisecatch方法来处理任何可能发生的promise拒绝。

有了这个简单的补充,上例中的代码将优雅地处理错误:

const contents = await getFileContents('this-file-does-not-exist.md', 'utf-8')
.catch((error) => {
console.log('Something went wrong!');
console.warn(error);
});
console.log(contents);

现在输出是这样子的:

Something went wrong!
[Error: ENOENT: no such file or directory, open 'this-file-does-not-exist.md'] {
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: 'this-file-does-not-exist.md'
}
undefined

至于使用哪种策略,我同意Valeri Karpov的建议。使用try/catch来恢复async函数内部的预期错误,但通过在调用函数中添加catch()来处理意外错误。

并行运行异步命令

当我们使用await关键字来等待一个异步操作完成时,JavaScript解释器会相应地暂停执行。虽然这很方便,但这可能并不总是我们想要的。考虑一下下面的代码:

(async () => {
async function getStarCount(repo){
const repoData = await fetch(repo);
const repoJson = await repoData.json()
return repoJson.stargazers_count;
} const reactStars = await getStarCount('https://api.github.com/repos/facebook/react');
const vueStars = await getStarCount('https://api.github.com/repos/vuejs/core');
console.log(`React has ${reactStars} stars, whereas Vue has ${vueStars} stars`)
})();

这里我们正在进行两次API调用,分别获取React和Vue的GitHub star数。虽然这样可以正常运转,但我们没有理由在发出第二个fetch请求之前等待第一个promise完成。如果我们要发出很多请求,这将是一个相当大的瓶颈。

为了解决这个问题,我们可以使用Promise.all,它接收一个promise数组,并等待所有promise被解决或其中任何一个承诺被拒绝:

(async () => {
async function getStarCount(repo){
// As before
} const reactPromise = getStarCount('https://api.github.com/repos/facebook/react');
const vuePromise = getStarCount('https://api.github.com/repos/vuejs/core');
const [reactStars, vueStars] = await Promise.all([reactPromise, vuePromise]); console.log(`React has ${reactStars} stars, whereas Vue has ${vueStars} stars`);
})();

好多了!

同步循环中的异步await

在某些时候,我们会尝试在一个同步循环中调用一个异步函数。比如说:

// Return promise which resolves after specified no. of milliseconds
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); async function process(array) {
array.forEach(async (el) => {
await sleep(el); // we cannot await promise here
console.log(el);
});
} const arr = [3000, 1000, 2000];
process(arr);

这不会像预期的那样奏效,因为forEach只会调用函数而不等待它完成,以下内容将被打印到控制台:

1000
2000
3000

同样的事情也适用于其他许多数组方法,如mapfilterreduce

幸运的是,ES2018引入了异步迭代器,除了它们的next()方法会返回一个promise外,它们就像普通的迭代器。这意味着我们可以在其中使用 await。让我们使用for...of重写上面的代码:

async function process(array) {
for (el of array) {
await sleep(el);
console.log(el);
};
}

现在,process函数的输出就是正确的顺序:

3000
1000
2000

就像我们之前等待异步fetch请求的例子一样,这也会带来性能上的代价。for循环中的每个await都会阻塞事件循环,通常应该重构代码,一次性创建所有的promise,然后使用Promise.all()来获取结果。

甚至有一条ESLint规则,如果它检测到这种行为就会警告。

顶层await

最后,让我们来看看一个叫做顶层await的东西。这是ES2022中引入的语言,从14.8版开始在Node中可用。

当我们在文章开头运行我们的代码时,我们已经被这个东西所要解决的问题给缠住了。还记得这个错误吗?

Uncaught SyntaxError: await is only valid in async functions, async generators and modules

当我们试图在一个async函数之外使用await时,就会发生这种情况。例如,在我们代码的顶层:

const ms = await Promise.resolve('Hello, World!');
console.log(msg);

顶层await解决了这个问题,使上述代码有效,但只在ES模块中奏效。如果我们在浏览器中工作,我们可以把这段代码添加到一个叫做index.js的文件中,然后像这样把它加载到我们的页面中:

<script src="index.js" type="module"></script>

事情会像预期的那样工作,不需要包装函数或丑陋的IIFE。

在Node中,事情变得更加有趣。要将一个文件声明为ES模块,我们应该做两件事中的一件。一种方法是以.mjs为扩展名保存,然后像这样运行它:

node index.mjs

另一种方法是在package.json文件中设置"type": "module"

{
"name": "myapp",
"type": "module",
...
}

顶层 await 也可以和动态导入很好地配合--一种类函数的表达式,它允许我们异步加载 ES 模块。这将返回一个promise,而这个promise将被解析为一个模块对象,这意味着我们可以这样做:

const locale = 'DE';

const { default: greet } = await import(
`${ locale === 'DE' ?
'./de.js' :
'./en.js'
}`
); greet();
// Outputs "Hello" or "Guten Tag" depending on the value of the locale variable

动态导入选项也很适合与React和Vue等框架相结合的懒加载。这使我们能够减少初始包的大小和交互指标的时间。

总结

在这篇文章中,我们研究了如何使用async/await来管理你的JavaScript程序的控制流。我们讨论了语法、async/await如何工作、错误处理,以及一些问题。如果你已经走到了这一步,你现在就是一个专家了。

编写异步代码可能很难,特别是对初学者来说,但现在你已经对这些技术有了扎实的了解,你应该能够运用它们来获得巨大的效果。

以上就是本文的全部内容,如果对你有所帮助,欢迎点赞、收藏、转发~

async/await初学者指南的更多相关文章

  1. Python Async/Await入门指南

    转自:https://zhuanlan.zhihu.com/p/27258289 本文将会讲述Python 3.5之后出现的async/await的使用方法,以及它们的一些使用目的,如果错误,欢迎指正 ...

  2. Python PEP 492 中文翻译——协程与async/await语法

    原文标题:PEP 0492 -- Coroutines with async and await syntax 原文链接:https://www.python.org/dev/peps/pep-049 ...

  3. node.js异步控制流程 回调,事件,promise和async/await

    写这个问题是因为最近看到一些初学者用回调用的不亦乐乎,最后代码左调来又调去很不直观. 首先上结论:推荐使用async/await或者co/yield,其次是promise,再次是事件,回调不要使用. ...

  4. 异步编程(async&await)

    前言 本来这篇文章上个月就该发布了,但是因为忙 QuarkDoc 一直没有时间整理,所以耽搁到今天,现在回归正轨. C# 5.0 虽然只引入了2个新关键词:async和await.然而它大大简化了异步 ...

  5. MVC+Spring.NET+NHibernate .NET SSH框架整合 C# 委托异步 和 async /await 两种实现的异步 如何消除点击按钮时周围出现的白线? Linq中 AsQueryable(), AsEnumerable()和ToList()的区别和用法

    MVC+Spring.NET+NHibernate .NET SSH框架整合   在JAVA中,SSH框架可谓是无人不晓,就和.NET中的MVC框架一样普及.作为一个初学者,可以感受到.NET出了MV ...

  6. 【转】C# Async/Await 异步编程中的最佳做法

    Async/Await 异步编程中的最佳做法 Stephen Cleary 近日来,涌现了许多关于 Microsoft .NET Framework 4.5 中新增了对 async 和 await 支 ...

  7. C# async await 死锁问题总结

    可能发生死锁的程序类型 1.WPF/WinForm程序 2.asp.net (不包括asp.net mvc)程序 死锁的产生原理 对异步方法返回的Task调用Wait()或访问Result属性时,可能 ...

  8. 深入理解协程(四):async/await异步爬虫实战

    本文目录: 同步方式爬取博客标题 async/await异步爬取博客标题 本片为深入理解协程系列文章的补充. 你将会在从本文中了解到:async/await如何运用的实际的爬虫中. 案例 从CSDN上 ...

  9. 温故知新,CSharp遇见异步编程(Async/Await),聊聊异步编程最佳做法

    什么是异步编程(Async/Await) Async/Await本质上是通过编译器实现的语法糖,它让我们能够轻松的写出简洁.易懂.易维护的异步代码. Async/Await是C# 5引入的关键字,用以 ...

  10. 答应我,这次必须搞懂!痛点难点Promise。(小点心async/await,基于Promise的更优方案)

    Promise 出现的原因 在 Promise 出现以前,我们处理一个异步网络请求,大概是这样: // 请求 代表 一个异步网络调用. // 请求结果 代表网络请求的响应. 请求1(function( ...

随机推荐

  1. win10环境下 VMware Workstation Pro 安装centos7无法上网

    一.安装centos7 网上类似的教程太多了,我就不一一写了,提供两个网址,先按照教程安装 VMware Workstation Pro ,秘钥在第二个链接里面(亲测可用), 安装完VMware在根据 ...

  2. win11 计算器的进制转换

  3. java Builder模式

    Builder 模式也叫建造者模式,builder模式的作用将一个复杂对象的构建与他的表示分离,一步一步创建一个复杂对象的创建型模式.在不知道内部建造细节的情况下,可以更精细的控制对象的构造流程.目的 ...

  4. 极速进化,光速转录,C++版本人工智能实时语音转文字(字幕/语音识别)Whisper.cpp实践

    业界良心OpenAI开源的Whisper模型是开源语音转文字领域的执牛耳者,白璧微瑕之处在于无法通过苹果M芯片优化转录效率,Whisper.cpp 则是 Whisper 模型的 C/C++ 移植版本, ...

  5. mosn基于延迟负载均衡算法 -- 走得更快,期待走得更稳

    前言 这篇文章主要是介绍mosn在v1.5.0中新引入的基于延迟的负载均衡算法. 对分布式系统中延迟出现的原因进行剖析 介绍mosn都通过哪些方法来降低延迟 构建来与生产环境性能分布相近的测试用例来对 ...

  6. ET中热更(ILRuntime)使用过程中,需要做的适配器,比如Linq排序

    ET中热更(ILRuntime)使用过程中,需要做的适配器,比如Linq排序 By Flamesky 最近项目中用到个Linq的排序,由于没有注册适配器,导致不能用,其实ILRT作者已经做得很好,报错 ...

  7. 2020-11-27:go中,map的读流程是什么?

    福哥答案2020-11-27:[答案来自此链接:](https://www.bilibili.com/video/BV1Nr4y1w7aa?p=12)源码位于runtime/map.go文件中的map ...

  8. 2021-04-15:给定一个由字符串组成的数组strs,必须把所有的字符串拼接起来,返回所有可能的拼接结果中,字典序最小的结果。

    2021-04-15:给定一个由字符串组成的数组strs,必须把所有的字符串拼接起来,返回所有可能的拼接结果中,字典序最小的结果. 福大大 答案2021-04-15: "b"和&q ...

  9. drf-spectacular

    介绍 drf-spectacular是为Django REST Framework生成合理灵活的OpenAPI 3.0模式.它可以自动帮我们提取接口中的信息,从而形成接口文档,而且内容十分详细,再也不 ...

  10. L1-025 正整数A+B (15 分)

    1.题目描述: 题的目标很简单,就是求两个正整数A和B的和,其中A和B都在区间[1,1000].稍微有点麻烦的是,输入并不保证是两个正整数. 输入格式: 输入在一行给出A和B,其间以空格分开.问题是A ...