zoukankan      html  css  js  c++  java
  • ES6 Generators的异步应用

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

  • 相关阅读:
    MongoDB 释放磁盘空间 db.runCommand({repairDatabase: 1 })
    RK 调试笔记
    RK Android7.1 拨号
    RK Android7.1 移植gt9271 TP偏移
    RK Android7.1 定制化 itvbox 盒子Launcher
    RK Android7.1 双屏显示旋转方向
    RK Android7.1 设置 内存条作假
    RK Android7.1 设置 蓝牙 已断开连接
    RK Android7.1 进入Camera2 亮度会增加
    RK 3128 调触摸屏 TP GT9XX
  • 原文地址:https://www.cnblogs.com/jaxu/p/6493291.html
Copyright © 2011-2022 走看看