ES6 Generators的异步应用

  ES6 Generators系列:

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

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

  Generators最主要的特色就是单线程执行,同步风格的代码编写,同时又容许你将代码的异步特性隐藏在程序的实现细节中。这使得咱们能够用很是天然的方式来表达程序或代码的流程,而不用同时还要兼顾如何编写异步代码。git

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

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

 

最简单的异步

  一个最简单的例子,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(); // 开始

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

  方法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的强大功能。敬请关注!

相关文章
相关标签/搜索