使用示例在JavaScript中解析异步/等待



本文的作者使用示例分析了JavaScript中的Async / Await。 通常,Async / Await是编写异步代码的便捷方法。 在此机会之前,使用回调和Promise编写了类似的代码。 原始文章的作者通过研究各种示例来揭示Async / Await的好处。

我们提醒您: 对于所有“ Habr”读者来说,使用“ Habr”促销代码注册任何Skillbox课程时均可享受10,000卢布的折扣。

Skillbox建议: Java开发人员在线教育课程。

回叫


回调是一个函数,其调用会无限期延迟。 以前,回调用于那些无法立即获得结果的代码部分。

这是在Node.js上异步读取文件的示例:

fs.readFile(__filename, 'utf-8', (err, data) => { if (err) { throw err; } console.log(data); }); 

当您需要一次执行多个异步操作时,就会出现问题。 让我们想象一下这种情况:向Arfat用户数据库发出请求,您需要读取其profile_img_url字段并从someserver.com服务器下载图片。
下载后,将图像转换为其他格式,例如,从PNG转换为JPEG。 如果转换成功,则会向用户的邮件发送一封电子邮件。 此外,有关事件的信息与日期一起输入到Transformations.log文件中。



值得在代码的最后部分中强加回调和大量的}。 这称为末日回响地狱或金字塔。

这种方法的缺点很明显:

  • 此代码很难阅读。
  • 还很难处理其中的错误,这通常会导致代码质量下降。

为了解决此问题,向JavaScript添加了promise。 它们使您可以将.then替换为回调的深层嵌套。



承诺的积极之处在于,有了它们,从上到下,而不是从左到右,代码阅读得更好。 然而,承诺也存在以下问题:

  • 需要添加大量的.then。
  • .catch代替try / catch来处理所有错误。
  • 在一个周期内处理多个promise并非总是很方便;在某些情况下,它们会使代码复杂化。

这是一个任务,将显示最后一段的含义。

假设有一个for循环,以随机间隔(0 – n秒)打印从0到10的数字序列。 使用promise,您需要更改此周期,以便以从0到10的顺序显示数字。因此,如果零输出花费6秒而单位花费2秒,则必须先输出零,然后才开始倒数输出。

当然,要解决此问题,我们不使用Async / Await或.sort。 最后是一个解决方案的例子。

异步功能


向ES2017(ES8)添加异步功能已简化了使用Promise的任务。 我注意到异步功能在promise之上起作用。 这些功能并不代表质的不同概念。 异步功能被认为是使用Promise的代码的替代方法。

Async / Await使得以异步方式组织异步代码的工作成为可能。

因此,对Promise的了解使您更容易理解Async / Await的原理。

句法

在典型情况下,它由两个关键字组成:async和await。 第一个单词使函数异步。 这些功能允许等待。 在任何其他情况下,使用此功能都将导致错误。

 // With function declaration async function myFn() { // await ... } // With arrow function const myFn = async () => { // await ... } function myFn() { // await fn(); (Syntax Error since no async) } 

异步插入在函数声明的最开始,对于箭头函数,插入在“ =”符号和方括号之间。

这些函数可以作为方法放置在对象中,也可以在类声明中使用。

 // As an object's method const obj = { async getName() { return fetch('https://www.example.com'); } } // In a class class Obj { async getResource() { return fetch('https://www.example.com'); } } 

注意! 值得记住的是,类的构造函数和getters / setter方法不能异步进行。

语义和执行规则

异步函数基本上类似于标准JS函数,但是也有例外。

因此,异步函数总是返回promise:

 async function fn() { return 'hello'; } fn().then(console.log) // hello 

特别是,fn返回字符串hello。 好吧,由于这是一个异步函数,因此使用构造函数将字符串值包装在promise中。

这是没有异步的替代设计:

 function fn() { return Promise.resolve('hello'); } fn().then(console.log); // hello 

在这种情况下,承诺的归还是“手动”进行的。 异步函数总是将自己包装在新的Promise中。

如果返回值是原始值,则异步函数将返回一个值,并将其包装在promise中。 如果返回值是promise的对象,则其解决方案将在新promise中返回。

 const p = Promise.resolve('hello') p instanceof Promise; // true Promise.resolve(p) === p; // true 

但是,如果异步函数内部发生错误怎么办?

 async function foo() { throw Error('bar'); } foo().catch(console.log); 

如果未处理,则foo()将返回带有redject的promise。 在这种情况下,Promise.reject代替Promise.resolve将返回包含错误的信息。

不管返回什么,输出上的异步函数总是给出承诺。

每次等待时都会暂停异步功能。

等待会影响表达式。 因此,如果表达式是一个Promise,则异步函数将被挂起,直到执行Promise。 如果表达式不是promise,则将其通过Promise.resolve转换为promise,然后终止。

 // utility function to cause delay // and get random value const delayAndGetRandom = (ms) => { return new Promise(resolve => setTimeout( () => { const val = Math.trunc(Math.random() * 100); resolve(val); }, ms )); }; async function fn() { const a = await 9; const b = await delayAndGetRandom(1000); const c = await 5; await delayAndGetRandom(1000); return a + b * c; } // Execute fn fn().then(console.log); 

这是fn函数工作方式的描述。

  • 调用它之后,第一行从const a = await 9转换; 在const a =等待Promise.resolve(9);
  • 使用Await之后,函数的执行将被挂起,直到接收到它的值为止(在当前情况下为9)。
  • delayAndGetRandom(1000)暂停fn函数的执行,直到完成为止(1秒后)。 实际上,这将使fn功能停止1秒钟。
  • delayAndGetRandom(1000)通过resolve返回一个随机值,然后将其分配给变量b。
  • 好吧,变量c的情况类似于变量a的情况。 在那之后,一切停止一秒钟,但是现在delayAndGetRandom(1000)不返回任何内容,因为这不是必需的。
  • 结果,这些值由公式a + b * c计算。 结果使用Promise.resolve包装在一个Promise中,并由该函数返回。

这些暂停可能类似于ES6中的生成器,但这是有原因的

我们解决问题


好了,现在让我们看一下上面提到的问题的解决方案。



finishMyTask函数使用Await等待诸如queryDatabase,sendEmail,logTaskInFile等操作的结果。 如果我们将此决定与承诺的使用地点进行比较,相似之处将显而易见。 不过,带有Async / Await的版本大大简化了所有语法上的困难。 在这种情况下,没有太多的回调和链接,例如.then / .catch。

这是输出数字的一种解决方案,有两种选择。

 const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms)); // Implementation One (Using for-loop) const printNumbers = () => new Promise((resolve) => { let pr = Promise.resolve(0); for (let i = 1; i <= 10; i += 1) { pr = pr.then((val) => { console.log(val); return wait(i, Math.random() * 1000); }); } resolve(pr); }); // Implementation Two (Using Recursion) const printNumbersRecursive = () => { return Promise.resolve(0).then(function processNextPromise(i) { if (i === 10) { return undefined; } return wait(i, Math.random() * 1000).then((val) => { console.log(val); return processNextPromise(i + 1); }); }); }; 

这是使用异步功能的解决方案。

 async function printNumbersUsingAsync() { for (let i = 0; i < 10; i++) { await wait(i, Math.random() * 1000); console.log(i); } } 

错误处理

未处理的错误包含在被拒绝的承诺中。 但是,在异步函数中,可以使用try / catch构造执行同步错误处理。

 async function canRejectOrReturn() { // wait one second await new Promise(res => setTimeout(res, 1000)); // Reject with ~50% probability if (Math.random() > 0.5) { throw new Error('Sorry, number too big.') } return 'perfect number'; } 

canRejectOrReturn()是一个异步函数,要么成功(“完美数字”),要么失败并显示错误(“抱歉,数字太大”)。

 async function foo() { try { await canRejectOrReturn(); } catch (e) { return 'error caught'; } } 

由于可以在上面的示例中执行canRejectOrReturn,因此其自身不成功的终止将需要执行catch块。 结果,foo函数将以未定义(在try块中未返回任何内容)或捕获到错误的情况下结束。 结果,此函数不会失败,因为try / catch将处理foo函数本身。

这是另一个示例:

 async function foo() { try { return canRejectOrReturn(); } catch (e) { return 'error caught'; } } 

值得注意的是,在foo的示例中,返回了canRejectOrReturn。 在这种情况下,Foo要么以一个完美的数字结尾,要么返回错误(“抱歉,数字太大”)错误。 catch块将永远不会执行。

问题是foo返回从canRejectOrReturn传递的promise。 因此,foo函数的解决方案成为canRejectOrReturn的解决方案。 在这种情况下,代码将仅包含两行:

 try { const promise = canRejectOrReturn(); return promise; } 

但是,如果使用await然后一起返回,会发生什么情况:

 async function foo() { try { return await canRejectOrReturn(); } catch (e) { return 'error caught'; } } 

在上面的代码中,foo成功执行,同时出现了完美数字和错误。 没有失败。 但是foo将以canRejectOrReturn结尾,而不是以undefined结尾。 让我们通过删除return await canRejectOrReturn()行来确保这一点:

 try { const value = await canRejectOrReturn(); return value; } // … 

常见的错误和陷阱


在某些情况下,使用异步/等待可能会导致错误。

被遗忘的等待

这种情况经常发生-在承诺之前,忘记了await关键字:

 async function foo() { try { canRejectOrReturn(); } catch (e) { return 'caught'; } } 

如您所见,在代码中,既没有等待也没有返回。 因此,foo始终以undefined退出,没有1秒的延迟。 但是诺言将兑现。 如果给出错误或拒绝,则将调用UnhandledPromiseRejectionWarning。

回调中的异步功能

异步函数通常在.map或.filter中用作回调。 一个示例是fetchPublicReposCount(用户名)函数,该函数返回在GitHub上打开的存储库数量。 假设我们需要三个用户的指标。 这是此任务的代码:

 const url = 'https://api.github.com/users'; // Utility fn to fetch repo counts const fetchPublicReposCount = async (username) => { const response = await fetch(`${url}/${username}`); const json = await response.json(); return json['public_repos']; } 

我们需要帐户ArfatSalman,octocat,norvig。 在这种情况下,执行:

 const users = [ 'ArfatSalman', 'octocat', 'norvig' ]; const counts = users.map(async username => { const count = await fetchPublicReposCount(username); return count; }); 

您应该注意.map回调中的Await。 这里counts是一个promise数组,.map是每个指定用户的匿名回调。

过度一致地使用await

以以下代码为例:

 async function fetchAllCounts(users) { const counts = []; for (let i = 0; i < users.length; i++) { const username = users[i]; const count = await fetchPublicReposCount(username); counts.push(count); } return counts; } 

此处,将回购编号放置在count变量中,然后将此编号添加到counts数组中。 该代码的问题在于,在第一个用户数据从服务器到达之前,所有后续用户都将处于待机模式。 因此,在一瞬间,仅处理一个用户。

例如,如果处理一个用户大约需要300毫秒,那么对于所有用户来说,这已经是一秒钟,花费的时间线性地取决于用户数量。 但是由于获取回购单的数量并不相互依赖,因此可以并行处理。 这需要使用.map和Promise.all:

 async function fetchAllCounts(users) { const promises = users.map(async username => { const count = await fetchPublicReposCount(username); return count; }); return Promise.all(promises); } 

输入中的Promise.all会收到一个带有诺言返回的诺言数组。 完成数组中的所有promise或第一个redject之后的最后一个完成。 它们可能并非同时启动,可能会发生-为了确保同时启动,可以使用p-map。

结论


异步功能对开发变得越来越重要。 好吧,对于异步功能的自适应使用,值得使用Async Iterators 。 JavaScript开发人员应该精通这一点。

Skillbox建议:

Source: https://habr.com/ru/post/zh-CN458950/


All Articles