经过前面两篇文章,咱们已经对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示例代码中,无一例外都会遇到嵌套回调的问题(咱们称之为回调地狱)。到目前为止咱们还有一些东西没有考虑到:
上面的这些问题都是能够解决的,可是谁都不想每次都面对这些问题而后从头至尾地解决一遍。咱们须要一个功能强大的设计模式,可以做为一个可靠的而且能够重用的解决方案,应用到咱们的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 ); } } })(); }
几个关键的点:
如今咱们来看看如何使用它。
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函数中异步调用部分的代码,从而解决了在回调中所遇到的各类问题:
首先咱们来看一下错误处理:
// 假设:`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(..)工具来实现上面介绍的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中。这样,就能够自动地发送promises和async 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的强大功能。敬请关注!