摘要:換句話說,我們很好的對代碼的功能關注點進行了分離通過將使用消費值得地方函數中的邏輯和通過異步流程來獲取值迭代器的方法進行了有效的分離。但是現在我們通過來管理代碼的異步流程部分,我們解決了回調函數所帶來的反轉控制等問題。
ES6 Generators:完整系列本文翻譯自 Going Async With ES6 Generators
由于個人能力知識有限,翻譯過程中難免有紕漏和錯誤,還望指正Issue
The Basics Of ES6 Generators
Diving Deeper With ES6 Generators
Going Async With ES6 Generators
Getting Concurrent With ES6 Generators
到目前為止,你已經對ES6 generators有了初步了解并且能夠方便的使用它,是時候準備將其運用到真實項目中提高現有代碼質量。
Generator函數的強大在于允許你通過一些實現細節來將異步過程隱藏起來,依然使代碼保持一個單線程、同步語法的代碼風格。這樣的語法使得我們能夠很自然的方式表達我們程序的步驟/語句流程,而不需要同時去操作一些異步的語法格式。
換句話說,我們很好的對代碼的功能/關注點進行了分離:通過將使用(消費)值得地方(generator函數中的邏輯)和通過異步流程來獲取值(generator迭代器的next()方法)進行了有效的分離。
結果就是?不僅我們的代碼具有強大的異步能力, 同時又保持了可讀性和可維護性的同步語法的代碼風格。
那么我們怎么實現這些功能呢?
最簡單的異步實現最簡單的情況,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函數(不帶任何其他裝飾)來實現和上面代碼相同的功能,實現代碼如下:
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路由中獲取數據來立刻fulfillAjax請求,而不用真正的向后端請求數據。
我們可以改變我們的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代碼例子依然存在反轉控制的問題(啊,回調地獄)正如文章最初的嵌套回調函數例子一樣。到目前為止,我們應該已經明顯察覺到了上面的例子存在一些待完善的地方:
到目前為止沒有明確的錯誤處理機制,正如我們上一篇學習的文章,在發送Ajax請求的過程中我們可能檢測到錯誤(在某處),通過it.throw(..)方法將錯誤傳遞會generator函數,然后在generator函數內部通過try..catch模塊來處理該錯誤。但是,我們在“后面”將要手動處理更多工作(更多的代碼來處理我們的generator迭代器),如果在我們的程序中多次使用generators函數,這些錯誤處理代碼很難被復用。
如果makeAjaxCall(..)工具函數不受我們控制,碰巧它多次調用了回調函數,或者同時將成功值或者錯誤返回到generator函數中,等等。我們的generator函數就將變得極難控制(未捕獲的錯誤,意外的返回值等)。處理、阻止上述問題的發生很多都是一些重復的工作,同時也都不是輕輕松松能夠完成的。
很多時候我們需要同時并行處理多個任務(例如兩個并行的Ajax請求)。由于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 ); } } })(); }
需要注意的關鍵點:
我們自動的初始化了generator函數(創建了it迭代器),然后我們異步運行it來完成generator函數的執行(done: true)。
我們尋找被yield表達式傳遞出來的promise(啊,也就是執行it.next(..)方法后返回的對象中的value字段)。如此,我們通過在promise的then(..)方法中注冊函數來監聽器完成。
如果一個非promise值被傳遞出來,我們僅僅將該值原樣返回到generator函數內部,因此看上去立即重新啟動了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將其傳遞出去,然后重新啟動generator函數,直到函數執行完成- - 所有被""隱藏""的實現細節!實際上并沒有隱藏起來,只是和我們消費該異步流程的代碼(generator中的控制流程)隔離開來了。
通過等待yield出去的promise的完成,然后將fulfill的值通過it.next(..)方法傳遞回函數中,result1 = yield request(..)表達式就回獲取到正如先前一樣的請求值。
但是現在我們通過promises來管理generator代碼的異步流程部分,我們解決了回調函數所帶來的反轉控制等問題。通過generator+promises的模式我們“免費”解決上述所遇到的問題:
現在我們用易用的內部錯誤處理機制。在runGenerator(..)函數中我們并沒有提及,但是監聽promise的錯誤并非難事,我們只需通過it.throw(..)方法將promise捕獲的錯誤拋進generator函數內部,在函數內部通過try...catch模塊進行錯誤捕獲及處理。
promise給我們提供了可控性/可依賴性。不用擔心,也不用疑惑。
Promises擁有一些強大的抽象工具方法,利用這些方法可以自動處理一些復雜的“并行”任務等。
例如,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控制流程,正如上面代碼。
ES7 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的能力。敬請密切關注。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/87352.html
摘要:傳統的異步方法回調函數事件監聽發布訂閱之前寫過一篇關于的文章,里邊寫過關于異步的一些概念。內部函數就是的回調函數,函數首先把函數的指針指向函數的下一步方法,如果沒有,就把函數傳給函數屬性,否則直接退出。 Generator函數與異步編程 因為js是單線程語言,所以需要異步編程的存在,要不效率太低會卡死。 傳統的異步方法 回調函數 事件監聽 發布/訂閱 Promise 之前寫過一篇關...
摘要:而在中是迭代器生成器,被創造性的拿來做異步流程控制了。當執行的時候,并不執行函數體,而是返回一個迭代器。行代碼再看看文章開頭的行代碼首先生成一個迭代器,然后執行一遍,得到的是一個對象,里面再執行。 廣告招人:阿里巴巴招前端,在這里你可以享受大公司的福利和技術體系,也有小團隊的挑戰和成長空間。聯系: qingguang.meiqg at alibaba-inc.com 首先請原諒我的標題...
摘要:回調函數這是異步編程最基本的方法。對象對象是工作組提出的一種規范,目的是為異步編程提供統一接口。誕生后,出現了函數,它將異步編程帶入了一個全新的階段。 更多詳情點擊http://blog.zhangbing.club/Ja... Javascript 語言的執行環境是單線程的,如果沒有異步編程,根本沒法用,非卡死不可。 為了解決這個問題,Javascript語言將任務的執行模式分成兩種...
摘要:更好的異步編程上面的方法可以適用于那些比較簡單的異步工作流程。小結的組合目前是最強大,也是最優雅的異步流程管理編程方式。 訪問原文地址 generators主要作用就是提供了一種,單線程的,很像同步方法的編程風格,方便你把異步實現的那些細節藏在別處。這讓我們可以用一種很自然的方式書寫我們代碼中的流程和狀態邏輯,不再需要去遵循那些奇怪的異步編程風格。 換句話說,通過將我們generato...
摘要:關于協程和中的什么是協程進程和線程眾所周知,進程和線程都是一個時間段的描述,是工作時間段的描述,不過是顆粒大小不同,進程是資源分配的最小單位,線程是調度的最小單位。子程序就是協程的一種特例。 關于協程和 ES6 中的 Generator 什么是協程? 進程和線程 眾所周知,進程和線程都是一個時間段的描述,是CPU工作時間段的描述,不過是顆粒大小不同,進程是 CPU 資源分配的最小單位,...
閱讀 637·2021-11-24 09:39
閱讀 3481·2019-08-30 15:53
閱讀 2515·2019-08-30 15:44
閱讀 3240·2019-08-30 12:54
閱讀 2210·2019-08-29 12:23
閱讀 3307·2019-08-26 14:05
閱讀 2106·2019-08-26 13:36
閱讀 3436·2019-08-26 13:33