ES6中的异步编程:Generators函数+Promise:最强大的异步处理方式

访问原文地址javascript

generators主要做用就是提供了一种,单线程的,很像同步方法的编程风格,方便你把异步实现的那些细节藏在别处。这让咱们能够用一种很天然的方式书写咱们代码中的流程和状态逻辑,再也不须要去遵循那些奇怪的异步编程风格。java

换句话说,经过将咱们generator逻辑中的一些值的运算操做和和异步处理(使用generators的迭代器iterator)这些值实现细节分开来写,咱们能够很是好的把性能处理和业务关注点给分开。es6

结果呢?全部那些强大的异步代码,将具有跟同步编程同样的可读性和可维护性。ajax

那么咱们将如何完成这些壮举?编程

一个简单的异步功能

先从这个很是的简单的sample代码开始,目前generators并不须要去额外的处理一些这段代码尚未了的异步功能。数组

举例下,加入如今的异步代码已是这样的了:promise

function makeAjaxCall(url, cb) {
    //do some ajax fun
    //call cb(result) when complete
}

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(不添加任何decoration)去从新实现一遍,代码看这里:缓存

function request(url) {
    // this is where we're hiding the asynchronicity,
    // away from the main code of our generator
    // it.next() 是generators的迭代器
    makeAjaxCall(url, function(response) {
        it.next(response);
    });
}

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的iterator的next(...)方法。异步

先来看调用request(".."),你会发现这里根本没有return任何值(换句话来讲,这是undefined)。这不要紧,可是它跟咱们在这篇文章后面会讨论到的方法作对比是很重要的:我须要有效的在这里使用yield undefined.

这时咱们来调用yield(这时仍是一个undefined值),其实除了暂停一下咱们的generators以外没有作别的了。它将等待知道下次再调用it.next(..) 才恢复,咱们队列已经把它在安排在(做为一个回调)Ajax请求结束的时候。

可是当yield表达式执行返回结果后咱们作了什么?咱们把返回值赋值给了result1.。那为何yield会有从第一个Ajax请求返回的值?

这是由于当it.next(..)在Ajax的callback中调用的是偶,它传递Ajax的返回结果。这说明这个返回值发送到咱们的generators时,已经中间那句result1 = yield .. 给暂停下来了。

这个真的很酷很强大。实质上看,result1 = yield request(..)这句是请求数据,可是它彻底把异步逻辑在咱们面前藏起来了,至少不须要咱们在这里考虑这部分异步逻辑,它经过yield的暂停能力隐藏了异步逻辑,同时把generator恢复逻辑的功能分离到下一个yield函数中。这就让咱们的主要逻辑看上去很像一个同步请求方法。

第二句表达式result2 = yield result(..)也基本同样的做用,它将pauses与resumes传进去,输出一个咱们请求的值,也根本不须要对异步操做担忧。

固然,由于yield的存在,这里会有一个微妙的提示,在这个点上会发生一些神奇的事情(也称异步)。可是跟噩梦般的嵌套回调地狱(或者是promise链的API开销)相比,yield语句只是须要一个很小的语法开销。

上面的代码老是启动一个异步Ajax请求,可是若是没有作会发生什么?若是咱们后来更改了咱们程序中先前(预先请求)的Ajax返回的数据,该怎么办?或者咱们的程序的URL路由系统经过其余一些复杂的逻辑,能够当即知足Ajax请求,这时就能够不须要fetch数据从服务器了。

这样,咱们能够把request(..)代码稍微修改一下

var cache = {};

function request(url) {
    if(cache[url]) {
        // defer cache里面的数据对如今来讲是已经足够了
        // 执行下面
        setTimeout(function() {
            it.next(cache[url])
        }, 0);
    }
    else {
        makeAjaxCall(url, function(resp) {
            cache[url] = resp;
            it.next(resp);
        })
    }
}

注意:一句很奇妙、神奇的setTimeout(..0)放在了当缓存中已经请求过数据的处理逻辑中。若是咱们当即调用it.next(...),这样会发生一个error,这是由于generator尚未完成paused操做。咱们的函数首先要彻底调用request(..),这时才会启动yield的暂停。所以,咱们不能当即在request(..)中当即调用it.next(...),这是由于这时generator仍然在运行(yield并无执行)。可是咱们能够稍后一点调用it.next(...),等待如今的线程执行完毕,这就是setTimeout(..0)这句有魔性的代码放在这里的意义。咱们稍后还会有一个更好的解决办法。

如今,咱们的generator代码并不须要发生任何变化:

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

看到没?咱们的generator逻辑(也就是咱们的流程逻辑)即便增长了缓存处理功能后,仍不须要发生任何改变。

*main()中的代码仍是只须要请求数据后暂停,以后等到数据返回后顺序执行下去。在咱们当前的状况下,‘暂停’可能相对要长一些(作一个服务器的请求,大约要300~800ms),或者他能够几乎当即返回(走setTimeout的逻辑),可是咱们的流程逻辑彻底不须要关心这些。

这就是将异步编程抽象成更小细节的真正力量。

更好的异步编程

上面的方法能够适用于那些比较简单的异步generator工做流程。可是它将很快收到限制,所以咱们须要一些更强大的异步机制与咱们的generator来合做,这样才能够发挥出更强大的功能。那是什么机制:Promise。

早先的Ajax实例老是会收到嵌套回调的困扰,问题以下:

  • 1.没有明确的方法来处理请求error。咱们都知道,Ajax请求有时是会失败的,这时咱们须要使用generator中的it.throw(...),同时还须要使用try...catch来处理请求错误时的逻辑。可是这更可能是一些在后台(咱们那些在iterator中的代码)手动的工做。我须要一些能够服用的方法,放在咱们本身代码的generator中。

  • 2.假如makeAjaxCall(..)这段代码不在咱们的控制下了,或者他须要屡次调用回调,又或者它同时返回success与error,等等。这时咱们的generator的会变得乱七八糟(返回error实现,出现异常值,等等)。控制以及防止发生这类问题是须要花费大量的手工时间的,并且一点也不能即插即用。

  • 3.一般我须要执行并行执行任务(好比同时作2个Ajax请求)。因为generator yield机制都是逐步暂停,没法在同时运行另外一个或多个任务,他的任务必须一个一个的按顺序执行。所以,这不是太容易在一个generator中去操做多任务,咱们只能默默的在背后手撸大量的代码。

就像你看到的,全部的问题都被解决了。可是没人愿意每次都去反复的去实现一遍这些方法。咱们须要一种更强大的模式,专门设计出一个可信赖的,可重用的基于generator异步编程的解决方法。

什么模式?把promise与yield结合,使得能够在执行完成后恢复generator的流程。

让咱们稍微用promise修改下request(..),让yield返回一个promise。

function request(url) {
    //如今返回一个promise了
    return new Promise( function(resolve, reject) {
        makeAjaxCall(url, resolve);
    });
}

request(..)如今由一个promise构成,当Ajax请求完成后会返回这个promise,可是而后呢?

咱们须要控制generator的iterator,它将接受到yield返回的那个promise,同时经过next(...)恢复generator运行,并把他们传递下去,我增长了一个runGenerator(...)方法来作这件事。

//比较简单,没有error事件处理
funtion runGenerator(g) {
    var it = g(), retl
    
    //异步iterator遍历generator
    (function iterate(val) {
        //返回一个promise
        ret = it.next(val);
        
        if(!ret.done) {
            if('then' in ret.value) {
                //等待接收promise
                ret.value.then(iterate);
            }
            //获取当即就有的数据,不是promise了
            else {
                //避免同步操做
                setTimeout(function() {
                    iterate(ret.value);
                }, 0);
            }
        }
    })();
}

关键点 :

  • 自动初始化generator(直接建立它的iterator),而且异步递将他一直运行到结束(当done:true就不在执行)

  • 若是Promise被返回出来,这时咱们就等待到执行then(...)方法的时候再处理。

  • 若是是能够当即返回的数据,咱们直接把数据返回给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跟原先的彻底同样嘛。尽管咱们改用了promise,可是yield方法不须要有什么变化,由于咱们把那些逻辑都从咱们的流程管理中分离出去了。

尽管如今的yield是返回一个promise了,并把这个promise传递给下一个it.next(..),可是result1 = yield request(..)这句获得的值跟之前仍是同样的。

咱们如今开始用promise来管理generator中的异步代码,这样咱们就解决掉全部使用回调函数方法中会出现的反转/信任问题。因为咱们用了generator+promise的方法,咱们不须要增长任何逻辑就解决掉了以上全部问题

  • 咱们能够很容易的增长一个error异常处理。虽然不在runGenerator(...)中,可是很容易从一个promise中监听error,并它他们的逻辑写在it.throw(..)里面,这时咱们就能够用上try..catch`方法在咱们的generator代码中去获取和管理erros了。

  • 咱们获得到全部的 control/trustability,彻底不须要增长代码。

  • promise有着很强的抽象性,让咱们能够实现一些多任务的并行操做。

    好比:`Promise.all([ .. ])`就能够并行执行一个promise数组,yield虽然只能拿到一个promise,可是这个是全部子promise执行完毕以后的集合数组。

先让咱们看下error处理的代码:

function request(url) {
    return new Promise( function(resolve, reject) {
        //第一个参数是error
        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);
        retrun;
    }
    var data = JSON.parse(result1);
    
    try{
        var result2 = yield request('http://some.url.2?id='+data.id);
    }
    catch(err) {
        console.log('Error:' + err);
        retrun;
    }
    var resp = JSON.parse(result2);
    console.log("The value you asked for: " + resp.value);
});

若是执行url的fetch时promise被reject(请求失败,或者异常)了,promise会给generator抛出一个异常,经过try..catch语句能够获取到了。

如今,咱们让promise来处理更复杂的异步操做:

function request(url) {
    return new Promise( function(resolve, reject) {
        makeAjax(url, resolve);
    })
    //获取到返回的text值后,作一些处理。
    .then( function(text) {
        
            //若是咱们拿到的是一个url就把text提早出来在返回
            if(/^http?:\/\/.+/.text(text)) {
                return request(text);            }
            //若是咱们就是要一个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([ .. ]) 里面放了3个子promise,主promise完成后就会在runGenerator中恢复generator。子promise拿到的是一天重定向的url,咱们会把它丢给下一个request请求,而后获取到最终数据。

任何复杂的异步功能均可以被promise搞定,并且你还能够用generator把这些流程写的像同步代码同样。只要你让yield返回一个promise。

ES7 async

如今能够稍微提下ES7了,它更像把runGenerator(..)这个异步执行逻辑作了一层封装。

async funtion 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();

咱们直接调用main()就能够执行完全部的流程,不须要调用next,也不须要去实现runGenerator(..)之类的来管理promise逻辑。只须要把yield关键词换成await就能够告诉异步方法,咱们在这里须要等到一个promise后才会接着执行。

有了这些原生的语法支持,是否是很酷。

小结

generator + yielded promise(s)的组合目前是最强大,也是最优雅的异步流程管理编程方式。经过封装一层流执行逻辑,咱们能够自动的让咱们的generator执行结束,而且还能够像处理同步逻辑同样管理error事件。

在ES7中,咱们甚至连这一层封装都不须要写了,变得更方便

参考

相关文章
相关标签/搜索