gpt4 book ai didi

async/await初学者指南

转载 作者:我是一只小鸟 更新时间:2023-07-20 22:31:25 33 4
gpt4 key购买 nike

JavaScript中的 async 和 await 关键字提供了一种现代语法,帮助我们处理异步操作。在本教程中,我们将深入研究如何使用 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 .

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

这个功能是在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 可能会是三种状态之一: pending 、 fulfilled 、或者 rejected 。一个 promise 开始时处于 pending 状态。如果与该 promise 有关的行为成功了,该 promise 就被称为 fulfilled 。如果行为不成功,该 promise 就被称为 rejected 。一旦 promise 是 fulfilled 或者 rejected ,但不是 pending ,它也被认为是 settled .

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

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

                        
                          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 API 、 Clipboard API 、 Fetch API 、 MediaDevices 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 操作失败时, promise 的 reject 方法被调用, 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 的函数都可以利用 promise 的 catch 方法来处理任何可能发生的 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

                        
                      

同样的事情也适用于其他许多数组方法,如 map 、 filter 和 reduce .

幸运的是,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初学者指南的文章就讲到这里了,如果你想了解更多关于async/await初学者指南的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

33 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com