原文链接: https://ponyfoo.com/articles/understanding-javascript-async-await

作者: Nicolás Bevacqua


目前async / await特性并没有被添加到ES2016标准中,但不代表这些特性将来不会被加入到Javascript中。在我写这篇文章时,它已经到达第三版草案,并且正迅速的发展中。这些特性已经被IE Edge支持了,而且它将会到达第四版,届时该特性将会登陆其他浏览器 -- 为加入该语言的下一版本而铺路(也可以看看:TC39进程)。

我们听说特性已经有一段时间了,现在让我们深入它,并了解它是如何工作的。为了能够了解这篇文章的内容,你需要对promise和生成器对象有深厚的理解。这些资源或许可以帮到你。

使用Promise

让我们假设我们有像下面这样的代码。在这里我将一个HTTP请求包装在一个Promise对象中。这个Promise在成功时会返回body对象,被拒绝时会将原因err返回。它每次都会在本博客(原作者博客)中为一篇随机文章拉取html内容。

var request = require('request');

function getRandomPonyFooArticle () {
return new Promise((resolve, reject) => {
request('https://ponyfoo.com/articles/random', (err, res, body) => {
if (err) {
reject(err); return;
}
resolve(body);
});
});
}

上述的promise代码的典型用法是像下面写的这样。 在那里,我们新建了一个promise链来将HTML页面中的DOM对象的一个子集转换成Markdown,然后再转换成对终端友好的输出, 最终再使用console.log输出它。 永远要记得为你的promise添加.catch处理器。

var hget = require('hget');
var marked = require('marked');
var Term = require('marked-terminal'); printRandomArticle(); function printRandomArticle () {
getRandomPonyFooArticle()
.then(html => hget(html, {
markdown: true,
root: 'main',
ignore: '.at-subscribe,.mm-comments,.de-sidebar'
}))
.then(md => marked(md, {
renderer: new Term()
}))
.then(txt => console.log(txt))
.catch(reason => console.error(reason));
}

当代码运行后,这段代码将产生像以下截图所示的输出。

上面那段代码就是“比用回调函数更好”的写法,它能让你感觉像在按顺序的阅读代码。

使用生成器(generator)

过去,通过探索,我们发现生成器可以用一种“同步”合成的方法来获得html。即使现在的代码有一些同步写法,其中还是涉及相当多的包装,而且生成器可能不是最直截了当的达到我们期望结果的方法,最终可能无论如何我们都会坚持改为使用promise。

function getRandomPonyFooArticle (gen) {
var g = gen();
request('https://ponyfoo.com/articles/random', (err, res, body) => {
if (err) {
g.throw(err); return;
}
g.next(body);
});
} getRandomPonyFooArticle(function* printRandomArticle () {
var html = yield;
var md = hget(html, {
markdown: true,
root: 'main',
ignore: '.at-subscribe,.mm-comments,.de-sidebar'
});
var txt = marked(md, {
renderer: new Term()
});
console.log(txt);
});

“请记住,在使用promise时,你应该将yield调用包装在try/catch块中来保留我们添加的错误处理器”

不说你也知道,像这样使用生成器并不容易扩展。除了涉及直观的语法的混入,你的迭代代码会高度耦合到生成器函数中,这将会降低扩展性。这表示你在添加新的await表达式到生成器中时需要经常修改它。一个更好的替代方案是使用即将到来的Async函数

使用async/await

当Async函数终于落地时,我们将可以采取基于Promise的实现方法并使用它的优点,即像写同步生成器一样写异步。这种做法的另一个好处是你完全不需要再去修改getRandomPonyFooArticle 方法,在它返回一个承诺前,它会一直等待。

要注意的是,await只能在函数中用async关键字标记后才能使用 它的工作方式和生成器很相似,直到promise完成之前,会在你的上下文中暂停处理。如果等待表达式不是一个promise,它也会被改造成一个promise。

read();

async function read () {
var html = await getRandomPonyFooArticle();
var md = hget(html, {
markdown: true,
root: 'main',
ignore: '.at-subscribe,.mm-comments,.de-sidebar'
});
var txt = marked(md, {
renderer: new Term()
});
console.log(txt);
}

“再次, -- 跟生成器一样 -- 记住,你最好把`await`包装到`try/catch`中,这样你就可以在异步函数中对返回后的promise进行错误捕获和处理。”

此外,一个Async函数总是会返回一个Promise对象。 这个promise在出现无法捕获的异常时会被拒绝,否则它会处理async函数的返回值。这就允许我们调用一个async函数并混入常规的基于promise的扩展。以下例子展示了两个方法的结合(看看Babel的交互式解释器)。

async function asyncFun () {
var value = await Promise
.resolve(1)
.then(x => x * 3)
.then(x => x + 5)
.then(x => x / 2);
return value;
}
asyncFun().then(x => console.log(`x: ${x}`));
// <- 'x: 4'

回到前一个例子中,那表示我们可以从异步读取函数中返回文本,并且允许调用者使用promise或另一个Async函数进行扩展。 那样,你的读取函数将只需关注从Pony Foo上的随机文章中拉取终端可读的Markdown即可。

async function read () {
var html = await getRandomPonyFooArticle();
var md = hget(html, {
markdown: true,
root: 'main',
ignore: '.at-subscribe,.mm-comments,.de-sidebar'
});
var txt = marked(md, {
renderer: new Term()
});
return txt;
}

然后,你可以进一步在另一个Async函数中调用await read()

async function write () {
var txt = await read();
console.log(txt);
}

或者你可以只使用promise对象来进一步扩展。

read().then(txt => console.log(txt));

岔路

在异步代码流中,总是能遇到同时执行两个或更多任务的情况。当Async函数更容易编写异步代码后,它们也将自己依次传递给代码。 这就是说:代码在一个时刻只执行一个操作。一个包含多个await表达式的函数在promise对象执行完之前,在恢复执行和移动到下一个await表达式之前,会在每个await表达式处暂停一次, -- 就跟我们在生成器和yield关键字处观察到的情况一样。

你可以使用Promise.all来解决创建单个promise对象并进行等待的功能。 当然,最大的问题是从习惯于让所有东西都串行运行改成使用Promise.all, 否则这将给你的代码带来性能瓶颈。

下面的例子展示了你如何同时完成对三个不同的promise对象进行等待操作。特定的await操作符会暂停你的Async函数,和等待 Promise.all表达式一起,最终会被解析到一个结果数组中,我们可以使用析构函数逐个拉取该数组中的单个结果。

async function concurrent () {
var [r1, r2, r3] = await Promise.all([p1, p2, p3]);
}

在某些情况下, 可以用 await *来改动上述代码片段,让你不必用Promise.all来包装你的promise对象。Babel 5依然支持这种特性,但它已经从规格说明中移除(也已经从Babel 6中移除) -- 因为这些原因

async function concurrent () {
var [r1, r2, r3] = await* [p1, p2, p3];
}

你依然可以用类似all = Promise.all.bind(Promise)的代码来做些事情,来获得一个简洁的替代Promise.all的方法。在这之上的是,你可以对Promise.race做相同的事情,而这跟使用await*并不等价。

const all = Promise.all.bind(Promise);
async function concurrent () {
var [r1, r2, r3] = `await all([p1, p2, p3])`;
}

错误处理

要注意的是,在异步函数中,错误会被“默默的”吞噬 -- 就像在普通的Promise对象中一样。 除非我们围绕await表达式添加try/catch块 -- 而不管在暂停时,它们会在你的异步函数体中发生还是在它暂停时发生 -- promise对象会被拒绝并通过Async函数返回错误。

自然,这可以看作是一个能力: 你可以利用try/catch代码块,有些东西你无法用回调函数实现-- 但可以用Promise对象实现。 在这种情况下,Async函数就类似生成器,得益于函数执行暂停特性,你可以利用try/catch将异步流代码写成同步代码的样子。

此外, 你可以在Async函数外捕获这些异常, 只需要简单的对它们返回的promise对象添加.catch()方法调用。在promise对象中尝试用.catch方法来将try/catch错误处理组合起来是一种比较灵活的方法,但该方法也可能导致混乱并最终导致错误无法处理。

read()
.then(txt => console.log(txt))
.catch(reason => console.error(reason));

我们要小心谨慎并时刻提醒自己用不同的方法来让我们可以发现错误、处理错误或预防错误。

如今如何使用async/await

如今,有一种在你的代码中使用Async函数的方法是通过Babel。这涉及一系列模块,但只要你愿意,你总是可以拿出一个模块来将全部这些代码包装进去。我包含npm-run作为一个有用的方法,用于保持本地的所有东西都用包进行安装。

npm i -g npm-run
npm i -D \
browserify \
babelify \
babel-preset-es2015 \
babel-preset-stage-3 \
babel-runtime \
babel-plugin-transform-runtime echo '{
"presets": ["es2015", "stage-3"],
"plugins": ["transform-runtime"]
}' > .babelrc

在使用babelifyAsync函数提供支持时,以下命令会将example.js通过browserify进行编译。然后你就可以用管道将脚本传输给node执行,或将脚本保存到硬盘中。

npm-run browserify -t babelify example.js | node

深入阅读

Async函数规格草案出奇的短,并且应该能成为一个有趣的读物, 如果你热衷于学习更多这些即将到来的功能。

我已经粘贴了一段代码在下面, 它是为了帮助你理解async函数的内部是如何工作的。即使我们不可以填充新的关键字,它也可以帮助你理解在async/await的帷幕后面发生了什么事情。

“换句话说,它应该对学习异步函数内部原理非常有帮助,无论是对生成器还是promise。”

首先,下面的一小段代码展示了一个async函数如何通过常规的function关键字来简化声明过程,这将返回一个生成spawn 生成器函数的结果 -- 我们会认为await在语法上是和yield等价的。

async function example (a, b, c) {
example function body
} function example (a, b, c) {
return spawn(function* () {
example function body
}, this);
}

spawn中,promise会被代码包装起来并传入生成器函数中,通过用户代码串行的执行,并将值传递到你的“生成器”代码中(async函数的函数体中)。 在这个意义上,我们可以注意Async函数真的是生成器和primose对象之上的语法糖,这对于让你理解其中每一个环节是如何工作来说非常重要,这是为了让你对于混合、匹配、合并不同的异步代码流的写法有一个更好的理解。

function spawn (genF, self) {
return new Promise(function (resolve, reject) {
var gen = genF.call(self);
step(() => gen.next(undefined));
function step (nextF) {
var next;
try {
next = nextF();
} catch(e) {
// 执行失败,并拒绝promise对象
reject(e);
return;
}
if (next.done) {
// 执行成功,处理promise对象
resolve(next.value);
return;
}
// 未完成,以yield标记的promise对象呗中断,并在此执行step方法
Promise.resolve(next.value).then(
v => step(() => gen.next(v)),
e => step(() => gen.throw(e))
);
}
});
}

“高亮部分的代码可以帮助你理解`async/await`如何对生成器序列进行迭代处理(通过`await`表达式),将每个对象按包装序列中并放入一个promise对象,通过一步步按顺序的链接起来。当**序列完成或其中一个promise对象被拒绝**时,promise对象的返回动作将由底层生成器函数完成”

特别鸣谢 @ljharb, @jaydson, @calvinf, @ericclemmons, @sherman3ero, @matthewmolnar3以及@rauschma为这篇文章的草稿进行审阅。


第一次用博客园的markdown编辑器写博客,还不太顺手,对markdown也不够熟悉(其实我也感觉博客园的markdown解析怪怪的)。这篇文章的英语感觉比较难看懂,翻译不准确甚至错误的地方应该很多,希望大家不吝指正。

2016.09.05

谢谢观看。

[译]理解Javascript的异步等待的更多相关文章

  1. 理解Javascript的异步等待

    目前async / await特性并没有被添加到ES2016标准中,但不代表这些特性将来不会被加入到Javascript中.在我写这篇文章时,它已经到达第三版草案,并且正迅速的发展中.这些特性已经被I ...

  2. 深入理解 JavaScript 异步系列(1)—— 什么是异步

    前言 2014年秋季写完了<深入理解javascript原型和闭包系列>,已经帮助过很多人走出了 js 原型.作用域.闭包的困惑,至今仍能经常受到好评的留言. 很早之前我就总结了JS三座大 ...

  3. 深入理解 JavaScript 异步系列(1)——基础

    前言 2014年秋季写完了<深入理解javascript原型和闭包系列>,已经帮助过很多人走出了 js 原型.作用域.闭包的困惑,至今仍能经常受到好评的留言. 很早之前我就总结了JS三座大 ...

  4. 深入理解 JavaScript 异步——转载

    本文章转载于深入理解 JavaScript 异步 前言 2014年秋季写完了<深入理解javascript原型和闭包系列>,已经帮助过很多人走出了 js 原型.作用域.闭包的困惑,至今仍能 ...

  5. 【前端知识体系-JS相关】深入理解JavaScript异步和单线程

    1. 为什么JavaScript是单线程? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事.那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊. Jav ...

  6. 【译】理解JavaScript闭包——新手指南

    闭包是JavaScript中一个基本的概念,每个JavaScript开发者都应该知道和理解的.然而,很多新手JavaScript开发者对这个概念还是很困惑的. 正确理解闭包可以帮助你写出更好.更高效. ...

  7. 深入理解JavaScript运行机制

    深入理解JavaScript运行机制 前言 本文是写作在给团队新人培训之际,所以其实本文的受众是对JavaScript的运行机制不了解或了解起来有困难的小伙伴.也就是说,其实真正的原理和本文阐述的并不 ...

  8. JavaScript可否多线程? 深入理解JavaScript定时机制

    JavaScript的setTimeout与setInterval是两个很容易欺骗别人感情的方法,因为我们开始常常以为调用了就会按既定的方式执行, 我想不少人都深有同感, 例如 setTimeout( ...

  9. 深入理解JavaScript定时机制和定时器注意问题

    容易欺骗别人感情的JavaScript定时器 JavaScript的setTimeout与setInterval是两个很容易欺骗别人感情的方法,因为我们开始常常以为调用了就会按既定的方式执行, 我想不 ...

随机推荐

  1. 那些年黑了你的微软BUG

    本文为 Dennis Gao 原创技术文章,发表于博客园博客,未经作者本人允许禁止任何形式的转载. 前言 炎炎夏日,朗朗乾坤,30℃ 的北京,你还在 Coding 吗? 整个 7 月都在忙项目,还加了 ...

  2. ASP.NET MVC 视图(二)

    ASP.NET MVC 视图(二) 前言 上篇中对于视图引擎只是做了简单的演示,对于真正的理解视图引擎的工作过程可能还有点模糊,本篇将会对由MVC框架提供给我们的Razor视图引擎的整个执行过程做一个 ...

  3. Restful WebApi项目开发实践

    前言 踩过了一段时间的坑,现总结一下,与大家分享,愿与大家一起讨论. Restful WebApi特点 WebApi相较于Asp.Net MVC/WebForm开发的特点就是前后端完全分离,后端使用W ...

  4. 增强版字典DictionaryEx

    代码 public class DictionaryEx<TKey, TValue> : IDictionary<TKey, TValue> { /// <summary ...

  5. maven repo plugin archiver

    Failure to transfer org.apache.maven:maven-archiver:pom:2.4.1 from http://repo1.maven.org/xxx was ca ...

  6. Spark join 源码跟读记录

    PairRDDFunctions类提供了以下两个join接口,只提供一个参数,不指定分区函数时默认使用HashPartitioner;提供numPartitions参数时,其内部的分区函数是HashP ...

  7. [CentOs7]搭建ftp服务器(2)——添加用户

    摘要 上篇文章完成了ftp服务器的安装与匿名访问的内容,当然出于安全的考虑是不允许匿名访问服务器的,所以就有了本篇的内容 ,为ftp服务器添加用户,用改用户进行访问. vsftpd添加用户 FTP用户 ...

  8. 一步步构造自己的vue2.0+webpack环境

    前面vue2.0和webpack都已经有接触了些(vue.js入门,webpack入门之简单例子跑起来),现在开始学习如何构造自己的vue2.0+webpack环境. 1.首先新建一个目录vue-wk ...

  9. 图片放大功能插件及jquery.extend函数理解

    前端时间,产品提出社区评论中的图片需要有放大功能.感觉可以共用,所以就想整合一个插件,过程中也借鉴了一些例子. 分析下自己的代码思路: var scaleImg = function(opts) { ...

  10. 【转】string.Format对C#字符串格式化

    转自:http://blog.csdn.net/samsone/article/details/7556781 1.格式化货币(跟系统的环境有关,中文系统默认格式化人民币,英文系统格式化美元) str ...