ES6 Generators系列:

  1. ES6 Generators基本概念
  2. 深入研究ES6 Generators
  3. ES6 Generators的异步应用
  4. ES6 Generators并发

  通过前面两篇文章,我们已经对ES6 generators有了一些初步的了解,是时候来看看如何在实际应用中发挥它的作用了。

  Generators最主要的特点就是单线程执行,同步风格的代码编写,同时又允许你将代码的异步特性隐藏在程序的实现细节中。这使得我们可以用非常自然的方式来表达程序或代码的流程,而不用同时还要兼顾如何编写异步代码。

  也就是说,通过generator函数,我们将程序具体的实现细节从异步代码中抽离出来(通过next(..)来遍历generator函数),从而很好地实现了功能和关注点的分离。

  其结果就是代码易于阅读和维护,在编写上具有同步风格,但却支持异步特性。那如何才能做到这一点呢?

最简单的异步

  一个最简单的例子,generator函数内部不需要任何异步执行代码即可完成整个异步过程的调用。

  假设你有下面这段代码:

function makeAjaxCall(url,cb) {
// ajax请求
// 完成时调用cb(result)
} makeAjaxCall( "http://some.url.1", function(result1){
var data = JSON.parse( result1 ); makeAjaxCall( "http://some.url.2/?id=" + data.id, function(result2){
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
});
} );

  如果使用generator函数来实现上面代码的逻辑:

function request(url) {
// 这里的异步调用被隐藏起来了,
// 通过it.next(..)方法对generator函数进行迭代,
// 从而实现了异步调用与main方法之间的分离
makeAjaxCall( url, function(response){
it.next( response );
} );
// 注意:这里没有return语句!
} function *main() {
var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 ); var result2 = yield request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
} var it = main();
it.next(); // 开始

  解释一下上面的代码是如何运行的。

  方法request(..)是对makeAjaxCall(..)的封装,确保回调能够调用generator函数的next(..)方法。请注意request(..)方法中没有return语句(或者说返回了一个undefined值),后面我们会讲到为什么要这么做。

  Main函数的第一行,由于request(..)方法没有任何返回值,所以这里的yield request(..)表达式不会接收任何值进行计算,仅仅暂停了main函数的运行,直到makeAjaxCall(..)在ajax的回调中执行it.next(..)方法,然后恢复main函数的运行。那这里yield表达式的结果到底是什么呢?我们将什么赋值给了变量result1?在Ajax的回调中,it.next(..)方法将Ajax请求的返回值传入,这个值会被yield表达式返回给变量result1

  是不是很酷!这里,result1 = yield request(..)事实上就是为了得到ajax的返回结果,只不过这种写法将回调隐藏起来了,我们完全不用担心,因为其中具体的执行步骤就是异步调用。通过yield表达式的暂停功能,我们将程序的异步调用隐藏起来,然后在另一个函数(ajax的回调)中恢复对generator函数的运行,整个过程使得我们的main函数的代码看起来就像是在同步执行一样

  语句result2 = yield result(..)的执行过程与上面一样。代码执行过程中,有关generator函数的暂停和恢复完全是透明的,程序最终将我们想要的结果返回回来,而所有的这些都不需要我们将注意力放在异步代码的编写上。

  当然,代码中少不了yield关键字,这里暗示着可能会有一个异步调用。不过这和地狱般的嵌套回调(或者promise链)比起来,代码看起来要清晰很多。

  注意上面我说的yield关键字的地方是“可能”会出现一个异步调用,而不是一定会出现。在上面的例子中,程序每次都会去调用一个Ajax的异步请求,但如果我们修改了程序,将之前Ajax响应的结果缓存起来,情况会怎样呢?又或者我们在程序的URL请求路由中加入某些逻辑判断,使其立即就返回Ajax请求的结果,而不是真正地去请求服务器,情况又会怎样呢?

  我们将上面的代码改成下面这个版本:

var cache = {};

function request(url) {
if (cache[url]) {
// 延迟返回缓存中的数据,以保证当前执行线程运行完成
setTimeout( function(){
it.next( cache[url] );
}, 0 );
}
else {
makeAjaxCall( url, function(resp){
cache[url] = resp;
it.next( resp );
} );
}
}

  注意上面代码中的setTimeout(..)语句,它会延迟返回缓存中的数据。如果我们直接调用it.next(..)程序会报错,这是因为generator函数目前还不是处于暂停状态。主函数在调用完request(..)之后,generator函数才会处于暂停状态。所以,我们不能在request(..)函数内部立即执行it.next(..),因为此时的generator函数仍然处于运行中(即yield表达式还没有被处理)。不过我们可以稍后再调用it.next(..)setTimeout(..)语句将会在当前执行线程完成后立即执行,也就是在request(..)方法执行完后再执行,这正是我们想要的。下面我们会有更好的解决方案。

  现在,我们的main函数的代码依然是这样:

var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );
..

  瞧!我们的程序从不带缓存的版本改成了带缓存的版本,但是main函数却不用做任何修改。*main()函数依然只是请求一个值,然后暂停运行,直到请求返回一个结果,然后再继续运行。当前程序中,暂停的时间可能会比较长(实际Ajax请求大概会在300-800ms之间),但也可能是0(使用setTimeout(..0)延迟的情况)。无论是哪种情况,我们的主流程是不变的。

  这就是将异步过程抽象为实现细节的真正力量!

改进的异步

  以上方法仅适用于一些简单异步处理的generator函数,很快你就会发现在大多数实际应用中根本不够用,所以我们需要一个更强大的异步处理机制来匹配generator函数,使其能够发挥更大的作用。这个处理机制是什么呢?答案就是promises. 如果你对ES6 Promises还不了解,可以看看这里的一篇文章: http://blog.getify.com/promises-part-1/

  在前面的Ajax示例代码中,无一例外都会遇到嵌套回调的问题(我们称之为回调地狱)。到目前为止我们还有一些东西没有考虑到:

  1. 有关错误处理。在前一篇文章中我们已经介绍过如何在generator函数中处理错误,我们可以在Ajax的回调中判断是否出错,并通过it.throw(..)方法将错误传递给generator函数,然后在generator函数中使用try..catch语句来处理它。但这无疑会带来许多工作量,而且如果程序中有很多generator函数的话,代码也不容易重用。
  2. 如果makeAjaxCall(..)函数不在我们的控制范围内,并且它会多次调用回调,或者同时返回success和error等等,那么我们的generator函数将会陷于混乱(未处理的异常,返回意外的值等)。要解决这些问题,你可能需要做很多额外的工作,这显然很不方便。
  3. 通常我们需要“并行”来处理多个任务(例如同时发起两个Ajax请求),由于generator函数的yield只允许单个暂停,因此两个或多个yield不能同时运行,它们必须按顺序一个一个地运行。所以,在不编写大量额外代码的前提下,很难在generator函数的单个yield中同时处理多个任务。

  上面的这些问题都是可以解决的,但是谁都不想每次都面对这些问题然后从头到尾地解决一遍。我们需要一个功能强大的设计模式,能够作为一个可靠的并且可以重用的解决方案,应用到我们的generator函数的异步编程中。这种模式要能够返回一个promises,并且在完成之后恢复generator函数的运行。

  回想一下上面代码中的yield request(..)表达式,函数request(..)没有任何返回值,但实际上这里我们是不是可以理解为yield返回了一个undefined呢?

  我们将request(..)函数改成基于promises的,这样它会返回一个promise,所以yield表达式的计算结果也是一个promise而不是undefined

function request(url) {
// 注意:现在返回的是一个promise!
return new Promise( function(resolve,reject){
makeAjaxCall( url, resolve );
} );
}

  现在,request(..)函数会构造一个Promise对象,并在Ajax调用完成之后进行解析,然后返回一个promise给yield表达式。然后呢?我们需要一个函数来控制generator函数的迭代,这个函数会接收所有的这些yield promises然后恢复generator函数的运行(通过next(..)方法)。我们假设这个函数叫runGenerator(..)

// 异步调用一个generator函数直到完成
// 注意:这是最简单的情况,不包含任何错误处理
function runGenerator(g) {
var it = g(), ret; // 异步迭代给定的generator函数
(function iterate(val){
ret = it.next( val ); if (!ret.done) {
// 简单测试返回值是否是一个promise
if ("then" in ret.value) {
// 等待promise返回
ret.value.then( iterate );
}
// 立即执行
else {
// 避免同步递归调用
setTimeout( function(){
iterate( ret.value );
}, 0 );
}
}
})();
}

  几个关键的点:

  1. 程序会自动初始化generator函数(创建迭代器it),然后异步运行直到完成(done:true)。
  2. 查看yield是否返回一个promise(通过it.next(..)返回值中的value属性来查看),如果是,则等待promise中的then(..)方法执行完。
  3. 任何立即执行的代码(非promise类型)将会直接返回结果给generator函数,然后继续运行。

  现在我们来看看如何使用它。

runGenerator( function *main(){
var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 ); var result2 = yield request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
} );

  等等!这不是和本文一开始的那个generator函数一样吗?是的。不过在这个版本中,我们创建了promises并返回给yield,等promise完成之后恢复generator函数继续运行。所有这些操作都“隐藏”在实现细节中!不过不是真正的隐藏,我们只是将它从消费代码(这里指的是我们的generator函数中的流程控制)中分离出去而已。

  Yield接受一个promise,然后等待它完成之后返回最终的结果给it.next(..)。通过这种方式,语句result1 = yield request(..)能够得到和之前一样的结果。

  现在我们使用promises来管理generator函数中异步调用部分的代码,从而解决了在回调中所遇到的各种问题:

  1. 拥有内置的错误处理机制。虽然我们并没有在runGenerator(..)函数中显示它,但是从promise监听错误并非难事,一旦监听到错误,我们可以通过it.throw(..)将错误抛出,然后通过try..catch语句捕获和处理这些错误。
  2. 我们通过promises来控制所有的流程。这一点毋庸置疑。
  3. 在自动处理各种复杂的“并行”任务方面,promises拥有十分强大的抽象能力。例如,yield Promise.all([..])接收一个“并行”任务的promises数组,然后yield一个单个的promise(返回给generator函数处理),这个单个的promise会等待数组中所有的promises全部处理完之后才会开始,但这些promises的执行顺序无法保证。当所有的promises执行完后,yield表达式会接收到另外一个数组,数组中的值是每个promise返回的结果,按照promise被请求的顺序依次排列。

  首先我们来看一下错误处理:

// 假设:`makeAjaxCall(..)` 是“error-first”风格的回调(为了简洁,省略了部分代码)
// 假设:`runGenerator(..)` 也具备错误处理的功能(为了简洁,省略了部分代码) function request(url) {
return new Promise( function(resolve,reject){
// 传入一个error-first风格的回调函数
makeAjaxCall( url, function(err,text){
if (err) reject( err );
else resolve( text );
} );
} );
} runGenerator( function *main(){
try {
var result1 = yield request( "http://some.url.1" );
}
catch (err) {
console.log( "Error: " + err );
return;
}
var data = JSON.parse( result1 ); try {
var result2 = yield request( "http://some.url.2?id=" + data.id );
} catch (err) {
console.log( "Error: " + err );
return;
}
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
} );

  在request(..)函数中,makeAjaxCall(..)如果出错,会返回一个promise的rejection,并最终映射到generator函数的error(在runGenerator(..)函数中通过it.throw(..)方法抛出错误,这部分细节对于消费端来说是透明的),然后在消费端我们通过try..catch语句最终捕获错误。

  下面我们来看一下复杂点的使用promises异步调用的情况:

function request(url) {
return new Promise( function(resolve,reject){
makeAjaxCall( url, resolve );
} )
// 在ajax调用完之后获取返回值,然后进行下一步操作
.then( function(text){
// 查看返回值中是否包含URL
if (/^https?:\/\/.+/.test( text )) {
// 如果有则继续调用这个新的URL
return request( text );
}
// 否则直接返回调用的结果
else {
return text;
}
} );
} runGenerator( function *main(){
var search_terms = yield Promise.all( [
request( "http://some.url.1" ),
request( "http://some.url.2" ),
request( "http://some.url.3" )
] ); var search_results = yield request(
"http://some.url.4?search=" + search_terms.join( "+" )
);
var resp = JSON.parse( search_results ); console.log( "Search results: " + resp.value );
} );

  Promise.all([...])构造了一个promise对象,它接收三个子promises,当所有的子promises都完成之后,将返回的结果通过yield表达式传递给runGenerator(..)函数并恢复运行。在request(..)函数中,每个子promise通过链式操作对response的值进行解析,如果其中包含另一个URL则继续请求这个URL,如果没有则直接返回response的值。有关promise的链式操作可以查看这篇文章: http://blog.getify.com/promises-part-5/#the-chains-that-bind-us

  任何复杂的异步处理,你都可以通过在generator函数中使用yield promise来完成(或者promise的promise链式操作),这样代码具有同步风格,看起来更加简洁。这是目前最佳的处理方式。

runGenerator(..)工具库

  我们需要定义我们自己的runGenerator(..)工具来实现上面介绍的generator+promises模式。为了简单,我们甚至可以不用实现所有的功能,因为这其中有很多的细节需要处理,例如错误处理的部分。

  但是你肯定不想亲自来写runGenerator(..)函数吧?反正我是不想。

  其实有很多的开源库提供了promise/async工具,你可以免费使用。这里我就不去一一介绍了,推荐看看Q.spawn(..)co(..)等。

  这里我想介绍一下我自己写的一个工具库:asynquence的插件runner。因为我认为和其它工具库比起来,这个插件提供了一些独特的功能。我写过一个系列文章,是有关asynquence的,如果你有兴趣的话可以去读一读。

  首先,asynquence提供了一系列的工具来自动处理“error-first”风格的回调函数。看下面的代码:

function request(url) {
return ASQ( function(done){
// 这里传入了一个error-first风格的回调函数 - done.errfcb
makeAjaxCall( url, done.errfcb );
} );
}

  看起来是不是会好很多?

  接下来,asynquence的runner(..)插件消费了asynquence序列(异步调用序列)中的generator函数,因此你可以从序列的从上一步中传入消息,然后generator函数可以将这个消息返回,继续传到下一步,并且这其中的任何错误都将自动向上抛出,你不用自己去管理。来看看具体的代码:

// 首先调用`getSomeValues()`创建一个sequence/promise,
// 然后将sequence中的async链起来
getSomeValues() // 使用generator函数来处理获取到的values
.runner( function*(token){
// token.messages数组将会在前一步中赋值
var value1 = token.messages[0];
var value2 = token.messages[1];
var value3 = token.messages[2]; // 并行调用3个Ajax请求,并等待它们全部执行完(以任何顺序)
// 注意:`ASQ().all(..)`类似于`Promise.all(..)`
var msgs = yield ASQ().all(
request( "http://some.url.1?v=" + value1 ),
request( "http://some.url.2?v=" + value2 ),
request( "http://some.url.3?v=" + value3 )
); // 将message发送到下一步
yield (msgs[0] + msgs[1] + msgs[2]);
} ) // 现在,将前一个generator函数的最终结果发送给下一个请求
.seq( function(msg){
return request( "http://some.url.4?msg=" + msg );
} ) // 所有的全部执行完毕!
.val( function(result){
console.log( result ); // 成功,全部完成!
} ) // 或者,有错误发生!
.or( function(err) {
console.log( "Error: " + err );
} );

  Asynquence runner(..)从sequence的上一步中接收一个messages(可选)来启动generator,这样在generator中可以访问token.messages数组中的元素。然后,与我们上面演示的runGenerator(..)函数一样,runner(..)负责监听yield promise或者yield asynquence(一个ASQ().all(..)包含了所有并行的步骤),等待完成之后再恢复generator函数的运行。当generator函数运行完之后,最终的结果将会传递给sequence中的下一步。此外,如果这其中有错误发生,包括在generator函数体内产生的错误,都将会向上抛出或者被错误处理程序捕捉到。

  Asynquence试图将promises和generator融合到一起,使代码编写变得非常简单。只要你愿意,你可以随意地将任何generator函数与基于promise的sequence联系到一起。

ES7 async

  在ES7的计划中,有一个提案非常不错,它创建了另外一种function:async function。有点像generator函数,它会自动包装到一个类似于我们的runGenerator(..)函数(或者asynquence的runner(..)函数)的utility中。这样,就可以自动地发送promisesasync function并在它们执行完后恢复运行(甚至都不需要generator函数遍历器了!)。

  代码看起来就像这样:

async function main() {
var result1 = await request( "http://some.url.1" );
var data = JSON.parse( result1 ); var result2 = await request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
} main();

  Async function可以被直接调用(上面代码中的main()语句),而不用像我们之前那样需要将它包装到runGenerator(..)或者ASQ.runner(..)函数中。在函数内部,我们不需要yield,取而代之的是await(另一个新加入的关键字),它会告诉async function等待promise完成之后才会继续运行。将来我们会有更多的generator函数库都支持本地语法。

  是不是很酷?

  同时,像asynquence runner这样的库一样,它们会给我们在异步generator函数编程方面带来极大的便利。

总结

  一句话,generator + yield promise(s)模式功能是如此强大,它们一起使得对同步和异步的流程控制变得行运自如。伴随着使用一些包装库(很多现有的库都已经免费提供了),我们可以自动执行我们的generator函数直到所有的任务全部完成,并且包含了错误处理!

  在ES7中,我们很可能将会看到async function这种类型的函数,它使得我们在没有第三方库支持的情况下也可以做到上面说的这些(至少对于一些简单情况来说是可以的)。

  JavaScript的异步在未来是光明的,而且只会越来越好!我坚信这一点。

  不过还没完,我们还有最后一个东西需要探索:

  如果有两个或多个generators函数,如何让它们独立地并行运行,并且各自发送自己的消息呢?这或许需要一些更强大的功能,没错!我们管这种模式叫“CSP”(communicating sequential processes)。我们将在下一篇文章中探讨和揭秘CSP的强大功能。敬请关注!

ES6 Generators的异步应用的更多相关文章

  1. ES6 Generators并发

    ES6 Generators系列: ES6 Generators基本概念 深入研究ES6 Generators ES6 Generators的异步应用 ES6 Generators并发 如果你已经读过 ...

  2. 深入研究ES6 Generators

    ES6 Generators系列: ES6 Generators基本概念 深入研究ES6 Generators ES6 Generators的异步应用 ES6 Generators并发 如果你还不知道 ...

  3. ES6 Generators基本概念

    ES6 Generators系列: ES6 Generators基本概念 深入研究ES6 Generators ES6 Generators的异步应用 ES6 Generators并发 在JavaSc ...

  4. ES6 generators in depth 一(译)

    今天在学习redux-saga时,外部链接推荐了这篇文章ES6 generators in depth,所以翻译的同时也可以加深一下对Generator的理解. 这里对原文一些只能在高版本现代浏览器使 ...

  5. ES6系列文章 异步神器async-await

    关于异步处理,ES5的回调使我们陷入地狱,ES6的Promise使我们脱离魔障,终于.ES7的async-await带我们走向光明.今天就来学习一下 async-await. async-await和 ...

  6. ES6 Promise 让异步函数顺序执行

    应用 ES6 的 内置对象 Promise, 让异步函数 按顺序执行的例子 如下: 上边 是四个用Promise 处理过的 异步执行的函数: fn1.fn2.fn3.fn4 下面,让其按顺序执行 如下 ...

  7. 这个一个对ES6多个异步处理的并发继发思想的总结和理解

    1.首先我们需要理解的是js中for循环.forEach循环.map循环的一些差异性,直接说了为后面说到的提供一些依据 1.1 for循环最基本,也是最容易理解的. 1.2 forEach和map用法 ...

  8. ES6 系列之异步处理实战

    前言 我们以查找指定目录下的最大文件为例,感受从 回调函数 -> Promise -> Generator -> Async 异步处理方式的改变. API 介绍 为了实现这个功能,我 ...

  9. [ES6] Generators

    Example 1: function *topicList(){ yield "ES2015"; yield "Semi-colons: good or bad?&qu ...

随机推荐

  1. Python笔记·第六章——集合 (set) 的增删改查及 copy()方法

    简介: 集合是无序的,不重复的数据集合,它里面的元素是可哈希的(不可变类型),但是集合本身是不可哈希(所以集合做不了字典的键)的.以下是集合最重要的两点: 1.去重,把一个列表变成集合,就自动去重了. ...

  2. 1267 - Illegal mix of collations (gbk_chinese_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '=' | 1267 - Illegal mix of collations (gbk_chinese_ci,IMPLICIT) and (Latin,COERCIBL)

    select * FROM information_schema.columns WHERE table_schema = "databaseName" and collation ...

  3. Deming管理系列(2)——怎样开发度量能力

    watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdTAxMTI1MDQ1NQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQk ...

  4. Matlab人脸检測方法(Face Parts Detection)具体解释

    今天同学让我帮忙制作一个人脸表情识别的样本库,当中主要是对人脸进行裁剪,这里用到了一个相对较新的Matlab人脸检測方法Face Parts Detection.网上百度了一下发现关于Matlab人脸 ...

  5. 六、Spring Boot Controller使用

    在Controller中使用 @RestController 注解,该注解是spring 4.0引入的.查看源码可知其包含了 @Controller 和 @ResponseBody 注解.我们可以理解 ...

  6. 4.python迭代器生成器装饰器

    容器(container) 容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个地迭代获取,可以用in, not in关键字判断元素是否包含在容器中.通常这类数据结构把所有的元素存储在内存中 ...

  7. 写给自己的web总结——css篇(1)

    上一篇写了关于html的知识,算是小试牛刀,这次来尝试写一下css. 初步了解css css的全称为cascading style sheet-- 层叠样式表,通过编入代码来对html里的标签做出各种 ...

  8. 【java】计算一段代码执行时长java.lang.System类里的public static long currentTimeMillis()方法

    public class Test_currentTimeMillis { public static void main(String[] args) { long start=System.cur ...

  9. SQLMap安装步骤

    SQLMap是利用Python语言写的,所以需要将Python这个语言环境给安装上 : 1.首先下载Python(这里Python版本为2.7.2,可以下载不同或高版本的) 2.然后在下载sqlmap ...

  10. 用过的关于css的知识

    1.代码片段 让两个div并排起来显示. <div style="width:1000px; text-align:center;" id="content&quo ...