本文翻譯自 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的能力。敬請密切關注。