js异步请求

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

咱们据说特性已经有一段时间了,如今让咱们深刻它,并了解它是如何工做的。为了可以了解这篇文章的内容,你须要对promise和生成器对象有深厚的理解。这些资源或许能够帮到你。php

 

使用Promise

让咱们假设咱们有像下面这样的代码。在这里我将一个HTTP请求包装在一个Promise对象中。这个Promise在成功时会返回body对象,被拒绝时会将缘由err返回。它每次都会在本博客(原做者博客)中为一篇随机文章拉取html内容。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处理器。java

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)); }

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

代码输出

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

 

使用生成器(generator)

过去,经过探索,咱们发现生成器能够用一种“同步”合成的方法来得到html。即便如今的代码有一些同步写法,其中仍是涉及至关多的包装,并且生成器可能不是最直截了当的达到咱们指望结果的方法,最终可能不管如何咱们都会坚持改成使用promise。git

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块中来保留咱们添加的错误处理器”es6

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

 

使用async/await

当Async函数终于落地时,咱们将能够采起基于Promise的实现方法并使用它的优势,即像写同步生成器同样写异步。这种作法的另外一个好处是你彻底不须要再去修改getRandomPonyFooArticle方法,在它返回一个承诺前,它会一直等待。shell

要注意的是,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)) ); } }); }
相关文章
相关标签/搜索