
本文的作者使用示例分析了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。 第一个单词使函数异步。 这些功能允许等待。 在任何其他情况下,使用此功能都将导致错误。
异步插入在函数声明的最开始,对于箭头函数,插入在“ =”符号和方括号之间。
这些函数可以作为方法放置在对象中,也可以在类声明中使用。
注意! 值得记住的是,类的构造函数和getters / setter方法不能异步进行。
语义和执行规则异步函数基本上类似于标准JS函数,但是也有例外。
因此,异步函数总是返回promise:
async function fn() { return 'hello'; } fn().then(console.log)
特别是,fn返回字符串hello。 好吧,由于这是一个异步函数,因此使用构造函数将字符串值包装在promise中。
这是没有异步的替代设计:
function fn() { return Promise.resolve('hello'); } fn().then(console.log);
在这种情况下,承诺的归还是“手动”进行的。 异步函数总是将自己包装在新的Promise中。
如果返回值是原始值,则异步函数将返回一个值,并将其包装在promise中。 如果返回值是promise的对象,则其解决方案将在新promise中返回。
const p = Promise.resolve('hello') p instanceof Promise;
但是,如果异步函数内部发生错误怎么办?
async function foo() { throw Error('bar'); } foo().catch(console.log);
如果未处理,则foo()将返回带有redject的promise。 在这种情况下,Promise.reject代替Promise.resolve将返回包含错误的信息。
不管返回什么,输出上的异步函数总是给出承诺。
每次等待时都会暂停异步功能。
等待会影响表达式。 因此,如果表达式是一个Promise,则异步函数将被挂起,直到执行Promise。 如果表达式不是promise,则将其通过Promise.resolve转换为promise,然后终止。
这是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));
这是使用异步功能的解决方案。
async function printNumbersUsingAsync() { for (let i = 0; i < 10; i++) { await wait(i, Math.random() * 1000); console.log(i); } }
错误处理未处理的错误包含在被拒绝的承诺中。 但是,在异步函数中,可以使用try / catch构造执行同步错误处理。
async function canRejectOrReturn() {
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';
我们需要帐户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建议: