經過前面兩篇文章,咱們已經對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的強大功能。敬請關注!