本文翻译自 Going Async With ES6 Generatorsjavascript
因为我的能力知识有限,翻译过程当中不免有纰漏和错误,还望指正Issuejava
到目前为止,你已经对ES6 generators有了初步了解而且可以方便的使用它,是时候准备将其运用到真实项目中提升现有代码质量。git
Generator函数的强大在于容许你经过一些实现细节来将异步过程隐藏起来,依然使代码保持一个单线程、同步语法的代码风格。这样的语法使得咱们可以很天然的方式表达咱们程序的步骤/语句流程,而不须要同时去操做一些异步的语法格式。es6
换句话说,咱们很好的对代码的功能/关注点进行了分离:经过将使用(消费)值得地方(generator函数中的逻辑)和经过异步流程来获取值(generator迭代器的next()
方法)进行了有效的分离。github
结果就是?不只咱们的代码具备强大的异步能力, 同时又保持了可读性和可维护性的同步语法的代码风格。ajax
那么咱们怎么实现这些功能呢?编程
最简单的状况,generator函数不须要额外的代码来处理异步功能,由于你的程序也不须要这样作。后端
例如,让咱们假象你已经写下了以下代码:数组
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函数(不带任何其余装饰)来实现和上面代码相同的功能,实现代码以下:promise
function request(url) { // this is where we're hiding the asynchronicity, // away from the main code of our generator // `it.next(..)` is the generator's iterator-resume // call makeAjaxCall( url, function(response){ it.next( response ); } ); // Note: nothing returned here! } 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(); // get it all started
让我来解释下上面代码是如何工做的。
request(..)
帮助函数主要对普通的makeAjaxCall(..)
实用函数进行包装,保证在在其回调函数中调用generator迭代器的next(..)
方法。
在调用request(..)
的过程当中,你可能已经发现函数并无显式的返回值(换句话说,其返回undefined
)。这没有什么大不了的,可是与本文后面的方法相比,返回值就显得比较重要了。这儿咱们生效的yield undefined
。
当咱们代码执行到yield..
时(yield
表达式返回undefined
值),咱们仅仅在这一点暂停了咱们的generator函数而没有作其余任何事。等待着it.next(..)
方法的执行来从新启动该generator函数,而it.next()
方法是在Ajax获取数据结束后的回调函数(推入异步队列等待执行)中执行的。
咱们对yield..
表达式的结果作了什么呢?咱们将其结果赋值给了变量result1
。那么咱们是怎么将Ajax请求结果放到该yield..
表达式的返回值中的呢?
由于当咱们在Ajax的回调函数中调用it.next(..)
方法的时候,咱们将Ajax的返回值做为参数传递给next(..)
方法,这意味着该Ajax返回值传递到了generator函数内部,当前函数内部暂停的位置,也就是result1 = yield..
语句中部。
上面的代码真的很酷而且强大。本质上,result1 = yield request(..)
的做用是用来请求值,可是请求的过程几乎彻底对咱们不可见- -或者至少在此处咱们不用怎么担忧它 - - 由于底层的实现使得该步骤成为了异步操做。generator函数经过经过在yield
表达式中隐藏的暂停功能以及将从新启动generator函数的功能分离到另一个函数中,来实现了异步操做。所以在主要代码中咱们经过一个同步的代码风格来请求值。
第二句result2 = yield result()
(译者注:做者的笔误,应该是result2 = yield request(..)
)代码,和上面的代码工做原理几乎无异:经过明显的暂停和从新启动机制来获取到咱们请求的数据,而在generator函数内部咱们不用再为一些异步代码细节为烦恼。
固然,yield
的出现,也就微妙的暗示一些神奇(啊!异步)的事情可能在此处发生。和嵌套回调函数带来的回调地狱相比,yield
在语法层面上优于回调函数(甚至在API上优于promise的链式调用)。
须要注意上面我说的是“可能”。generator函数完成上面的工做,这自己就是一件很是强大的事情。上面的程序始终发送一个异步的Ajax请求,假如不发送异步Ajax请求呢?假若咱们改变咱们的程序来从缓存中获取到先前(或者预先请求)Ajax请求的结果?或者从咱们的URL路由中获取数据来马上fulfill
Ajax请求,而不用真正的向后端请求数据。
咱们能够改变咱们的request(..)
函数来知足上面的需求,以下:
var cache = {}; function request(url) { if (cache[url]) { // "defer" cached response long enough for current // execution thread to complete setTimeout( function(){ it.next( cache[url] ); }, 0 ); } else { makeAjaxCall( url, function(resp){ cache[url] = resp; it.next( resp ); } ); } }
注意:在上面的代码中咱们使用了一个细微的技巧setTimeout(..0)
,当从缓存中获取结果时来延迟代码的执行。若是咱们不延迟而是当即执行it.next(..)
方法,这将会致使错误的发生,由于(这就是技巧所在)此时generator函数尚未中止执行。首先咱们执行request(..)
函数,而后经过yield
来暂停generator函数。所以不可以在request(..)
函数中当即调用it.next(..)
方法,由于在此时,generator函数依然在运行(yield
尚未被调用)。可是咱们能够在当前线程运行结束后,当即执行it.next(..)
。这就是setTimeout(..0)
将要完成的工做。在文章后面咱们将看到一个更加完美的解答。
如今,咱们generator函数内部主要代码依然以下:
var result1 = yield request( "http://some.url.1" ); var data = JSON.parse( result1 ); ..
看到没!?当咱们代码从没有缓存到上面有缓存的版本,咱们generator函数内部逻辑(咱们的控制流程)居然没有变化。
*main()
函数内部代码依然是请求数据,暂停generator函数的执行来等待数据的返回,数据传回后继续执行。在咱们当前场景中,这个暂停
可能相对比较长(真实的向服务器发送请求,这可能会耗时300~800ms)或者几乎当即执行(使用setTimeout(..0)
手段延迟执行)。可是咱们*main
函数中的控制流程不用关心数据从何而来。
这就是从实现细节中将异步流程分离出来的强大力量。
利用上面说起的方法(回调函数),generators函数可以完成一些简单的异步工做。可是却至关局限,所以咱们须要一个更增强大的异步机制来与咱们的generator函数匹配结合。完成一些更加繁重的异步流程。什么异步机制呢?Promises。
若是你依然对ES6 Promises感到困惑,我写过关于Promise的系列文章。去阅读一下。我会等待你回来,<滴答,滴答>。老掉牙的异步笑话了。
先前的Ajax代码例子依然存在反转控制的问题(啊,回调地狱)正如文章最初的嵌套回调函数例子同样。到目前为止,咱们应该已经明显察觉到了上面的例子存在一些待完善的地方:
it.throw(..)
方法将错误传递会generator函数,而后在generator函数内部经过try..catch
模块来处理该错误。可是,咱们在“后面”将要手动处理更多工做(更多的代码来处理咱们的generator迭代器),若是在咱们的程序中屡次使用generators函数,这些错误处理代码很难被复用。makeAjaxCall(..)
工具函数不受咱们控制,碰巧它屡次调用了回调函数,或者同时将成功值或者错误返回到generator函数中,等等。咱们的generator函数就将变得极难控制(未捕获的错误,意外的返回值等)。处理、阻止上述问题的发生不少都是一些重复的工做,同时也都不是轻轻松松可以完成的。yield
表达式执行后都会暂停函数的执行,不可以同时运行两个或多个yield
表达式,也就是说yield
表达式只能按顺序一个接一个的运行。所以在没有大量手写代码的前提下,一个yield
表达式中同时执行多个任务依然不太明朗。正如你所见,上面的全部问题都能够被解决,可是又有谁愿意每次重复手写这些代码呢?咱们须要一种更增强大的模式,该模式是可信赖且高度复用的,而且可以很好的解决generator函数处理异步流程问题。
什么模式?yield 表达式内部是promise,当这些promise被fulfill后从新启动generator函数。
回忆上面代码,咱们使用yield request(..)
,可是request(..)
工具函数并无返回任何值,那么它仅仅yield undefined
吗?
让咱们稍微调整下上面的代码。咱们把request(..)
函数改成以promise为基础的函数,所以该函数返回一个promise,如今咱们经过yield
表达式返回了一个真实的promise(而不是undefined
)。
function request(url) { // Note: returning a promise now! return new Promise( function(resolve,reject){ makeAjaxCall( url, resolve ); } ); }
request(..)
函数经过构建一个promise来监听Ajax的完成而且resolve返回值,而且返回该promise,所以promise也可以被yield
传递到generator函数外部,接下来呢?
咱们须要一个工具函数来控制generator函数的迭代器,该工具函数接收yield
表达式传递出来的promise,而后在promie 状态转为fulfill或者reject时,经过迭代器的next(..)
方法从新启动generator函数。如今我为这个工具函数取名runGenerator(..)
:
// run (async) a generator to completion // Note: simplified approach: no error handling here function runGenerator(g) { var it = g(), ret; // asynchronously iterate over generator (function iterate(val){ ret = it.next( val ); if (!ret.done) { // poor man's "is it a promise?" test if ("then" in ret.value) { // wait on the promise ret.value.then( iterate ); } // immediate value: just send right back in else { // avoid synchronous recursion setTimeout( function(){ iterate( ret.value ); }, 0 ); } } })(); }
须要注意的关键点:
it
迭代器),而后咱们异步运行it
来完成generator函数的执行(done: true
)。yield
表达式传递出来的promise(啊,也就是执行it.next(..)
方法后返回的对象中的value
字段)。如此,咱们经过在promise的then(..)
方法中注册函数来监听器完成。如今咱们怎么使用它呢?
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
将其传递出去,而后从新启动generator函数,直到函数执行完成- - 全部被''隐藏''的实现细节!实际上并无隐藏起来,只是和咱们消费该异步流程的代码(generator中的控制流程)隔离开来了。
经过等待yield
出去的promise的完成,而后将fulfill的值经过it.next(..)
方法传递回函数中,result1 = yield request(..)
表达式就回获取到正如先前同样的请求值。
可是如今咱们经过promises来管理generator代码的异步流程部分,咱们解决了回调函数所带来的反转控制等问题。经过generator+promises的模式咱们“免费”解决上述所遇到的问题:
runGenerator(..)
函数中咱们并无说起,可是监听promise的错误并不是难事,咱们只需经过it.throw(..)
方法将promise捕获的错误抛进generator函数内部,在函数内部经过try...catch
模块进行错误捕获及处理。例如,yield Prmise.all([ .. ])
能够接受一个promise数组而后“并行”执行这些任务,而后yield
出去一个单独的promise(给generator函数处理),该promise将会等待全部并行的promise都完成后才被完成,你能够经过yield
表达式的返回数组(当promise完成后)来获取到全部并行promise的结果。数组中的结果和并行promises任务一一对应(所以其彻底忽略promise完成的顺序)。
首先,让咱们研究下错误处理:
// assume: `makeAjaxCall(..)` now expects an "error-first style" callback (omitted for brevity) // assume: `runGenerator(..)` now also handles error handling (omitted for brevity) function request(url) { return new Promise( function(resolve,reject){ // pass an error-first style callback 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 ); } );
当再URL 请求发出后一个promise被reject后(或者其余的错误或异常),这个promise的reject值将会映射到一个generator函数错误(经过runGenerator(..)
内部隐式的it.throw(..)
来传递错误),该错误将会被try..catch
模块捕获。
如今,让咱们看一个经过promises来管理更加错综复杂的异步流程的事例:
function request(url) { return new Promise( function(resolve,reject){ makeAjaxCall( url, resolve ); } ) // do some post-processing on the returned text .then( function(text){ // did we just get a (redirect) URL back? if (/^https?:\/\/.+/.test( text )) { // make another sub-request to the new URL return request( text ); } // otherwise, assume text is what we expected to get back 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来等待其内部的三个并行promise的完成,该新的promise将会被yield
表达式传递到外部给runGenerator(..)
工具函数中,runGenerator()
函数监听该新生成的promise的完成,以便从新启动generator函数。并行的promise的返回值可能会成为另一个URL的组成部分,而后经过yield
表达式将另一个promise传递到外部。关于更多的promise链式调用,参见文章
promise能够处理任何复杂的异步过程,你能够经过generator函数yield
出去promises(或者promise返回promise)来获取到同步代码的语法形式。(对于promise或者generator两个ES6的新特性,他们的结合或许是最好的模式)
runGenerator(..)
: 实用函数库在上面咱们已经定义了runGenerator(..)
工具函数来顺利帮助咱们充分发挥generator+promise模式的卓越能力。咱们甚至省略了(为了简略起见)该工具函数的完整实现,在错误处理方面依然有些细微细节咱们须要处理。
可是,你不肯意实现一个你本身的runGenerator(..)
是吗?
我不这么认为。
许多promise/async库都提供了上述工具函数。在此我不会一一论述,可是你一个查阅Q.spawn(..)
和co(..)
库,等等。
可是我会简要的阐述我本身的库asynquence中的runner(..)
插件,相对于其余库,我想提供一些独一无二的特性。若是对此感兴趣并想学习更多关于asynquence
的知识而不是浅尝辄止,能够看看之前的两篇文章深刻asynquence
首先,asynquence提供了自动处理上面代码片断中的”error-first-style“回调函数的工具函数:
function request(url) { return ASQ( function(done){ // pass an error-first style callback makeAjaxCall( url, done.errfcb ); } ); }
是否是看起来更加好看,不是吗!?
接下来,asynquence提供了runner(..)
插件来在异步序列(异步流程)中执行generator函数,所以你能够在runner
前面的步骤传递信息到generator函数内,同时generator函数也能够传递消息出去到下一个步骤中,同时如你所愿,全部的错误都自动冒泡被最后的or
所捕获。
// first call `getSomeValues()` which produces a sequence/promise, // then chain off that sequence for more async steps getSomeValues() // now use a generator to process the retrieved values .runner( function*(token){ // token.messages will be prefilled with any messages // from the previous step var value1 = token.messages[0]; var value2 = token.messages[1]; var value3 = token.messages[2]; // make all 3 Ajax requests in parallel, wait for // all of them to finish (in whatever order) // Note: `ASQ().all(..)` is like `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 ) ); // send this message onto the next step yield (msgs[0] + msgs[1] + msgs[2]); } ) // now, send the final result of previous generator // off to another request .seq( function(msg){ return request( "http://some.url.4?msg=" + msg ); } ) // now we're finally all done! .val( function(result){ console.log( result ); // success, all done! } ) // or, we had some error! .or( function(err) { console.log( "Error: " + err ); } );
asyquence的runner(..)
工具接受上一步序列传递下来的值(也有可能没有值)来启动generator函数,能够经过token.messages
数组来获取到传入的值。
而后,和上面咱们所描述的runGenerator(..)
工具函数相似,runner(..)
也会监听yield
一个promise或者yield
一个asynquence序列(在本例中,是指经过ASQ().all()
方法生成的”并行”任务),而后等待promise或者asynquence序列的完成后从新启动generator函数。
当generator函数执行完成后,最后经过yield
表达式传递的值将做为参数传递到下一个序列步骤中。
最后,若是在某个序列步骤中出现错误,甚至在generator内部,错误都会冒泡到被注册的or(..)
方法中进行错误处理。
asynquence经过尽量简单的方式来混合匹配promises和generator。你能够自由的在以promise为基础的序列流程后面接generator控制流程,正如上面代码。
async
在ES7的时间轴上有一个提案,而且有极大可能被接受,该提案将在JavaScript中添加另一个函数类型:async
函数,该函数至关于用相似于runGenerator(..)
(或者asynquence的runner(..)
)工具函数在generator函数外部包装一下,来使得其自动执行。经过async函数,你能够把promises传递到外部而后async函数在promises状态变为fulfill时自动从新启动直到函数执行完成。(甚至不须要复杂的迭代器参与)
async函数大概形式以下:
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 函数
能够想普通函数同样被调用(如main()
),而不须要包装函数如runGenerator(..)
或者ASQ().runner(..)
的帮助。同时,函数内部再也不使用yield
,而是使用await
(另一个JavaScript关键字)关键字来告诉async 函数
等待当前promise获得返回值后继续执行。
基本上,async函数拥有经过一些包装库调用generator函数的大部分功能,同时关键是其被原生语法所支持。
是否是很酷!?
同时,像asynquence这样的工具集使得咱们可以轻易的且充分利用generator函数完成异步工做。
简单地说:经过把promise和generator函数两个世界组合起来成为generator + yield promise(s)
模式,该模式具备强大的能力及同步语法形式的异步表达能力。经过一些简单包装的工具(不少库已经提供了这些工具),咱们可让generator函数自动执行完成,而且提供了健全和同步语法形式的错误处理机制。
同时在ES7+的未来,咱们也许将迎来async function
函数,async 函数将不须要上面那些工具库就可以解决上面遇到的那些问题(至少对于基础问题是可行的)!
JavaScript的异步处理机制的将来是光明的,并且会愈来愈光明!我要带墨镜了。(译者注:这儿是做者幽默的说法)
可是,咱们并无在这儿就结束本系列文章,这儿还有最后一个方面咱们想要研究:
假若你想要将两个或多个generator函数结合在一块儿,让他们独立平行的运行,而且在它们执行的过程当中来来回回得传递信息?这必定会成为一个至关强大的特性,难道不是吗?这一模式被称做“CSP”(communicating sequential processes)。咱们将在下面一篇文章中解锁CSP的能力。敬请密切关注。