訪問原文地址javascript
generators主要做用就是提供了一種,單線程的,很像同步方法的編程風格,方便你把異步實現的那些細節藏在別處。這讓咱們能夠用一種很天然的方式書寫咱們代碼中的流程和狀態邏輯,再也不須要去遵循那些奇怪的異步編程風格。java
換句話說,經過將咱們generator邏輯中的一些值的運算操做和和異步處理(使用generators的迭代器iterator)這些值實現細節分開來寫,咱們能夠很是好的把性能處理和業務關注點給分開。es6
結果呢?全部那些強大的異步代碼,將具有跟同步編程同樣的可讀性和可維護性。ajax
那麼咱們將如何完成這些壯舉?編程
先從這個很是的簡單的sample代碼開始,目前generators並不須要去額外的處理一些這段代碼尚未了的異步功能。數組
舉例下,加入如今的異步代碼已是這樣的了:promise
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(不添加任何decoration)去從新實現一遍,代碼看這裏:緩存
function request(url) { // this is where we're hiding the asynchronicity, // away from the main code of our generator // it.next() 是generators的迭代器 makeAjaxCall(url, function(response) { it.next(response); }); } 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(); //啓動全部請求
讓咱們捋一下這是如何工做的服務器
request(..)
函數對makeAjaxCall(..)
作了基本封裝,讓數據請求的回調函數中調用generator的iterator的next(...)
方法。異步
先來看調用request("..")
,你會發現這裏根本沒有return任何值(換句話來講,這是undefined)。這不要緊,可是它跟咱們在這篇文章後面會討論到的方法作對比是很重要的:我須要有效的在這裏使用yield undefined.
這時咱們來調用yield(這時仍是一個undefined值),其實除了暫停一下咱們的generators以外沒有作別的了。它將等待知道下次再調用it.next(..) 才恢復,咱們隊列已經把它在安排在(做爲一個回調)Ajax請求結束的時候。
可是當yield表達式執行返回結果後咱們作了什麼?咱們把返回值賦值給了result1.。那爲何yield會有從第一個Ajax請求返回的值?
這是由於當it.next(..)在Ajax的callback中調用的是偶,它傳遞Ajax的返回結果。這說明這個返回值發送到咱們的generators時,已經中間那句result1 = yield ..
給暫停下來了。
這個真的很酷很強大。實質上看,result1 = yield request(..)
這句是請求數據,可是它徹底把異步邏輯在咱們面前藏起來了,至少不須要咱們在這裏考慮這部分異步邏輯,它經過yield的暫停能力隱藏了異步邏輯,同時把generator恢復邏輯的功能分離到下一個yield函數中。這就讓咱們的主要邏輯看上去很像一個同步請求方法。
第二句表達式result2 = yield result(..)
也基本同樣的做用,它將pauses與resumes傳進去,輸出一個咱們請求的值,也根本不須要對異步操做擔憂。
固然,由於yield的存在,這裏會有一個微妙的提示,在這個點上會發生一些神奇的事情(也稱異步)。可是跟噩夢般的嵌套回調地獄(或者是promise鏈的API開銷)相比,yield語句只是須要一個很小的語法開銷。
上面的代碼老是啓動一個異步Ajax請求,可是若是沒有作會發生什麼?若是咱們後來更改了咱們程序中先前(預先請求)的Ajax返回的數據,該怎麼辦?或者咱們的程序的URL路由系統經過其餘一些複雜的邏輯,能夠當即知足Ajax請求,這時就能夠不須要fetch數據從服務器了。
這樣,咱們能夠把request(..)
代碼稍微修改一下
var cache = {}; function request(url) { if(cache[url]) { // defer cache裏面的數據對如今來講是已經足夠了 // 執行下面 setTimeout(function() { it.next(cache[url]) }, 0); } else { makeAjaxCall(url, function(resp) { cache[url] = resp; it.next(resp); }) } }
注意:一句很奇妙、神奇的setTimeout(..0)
放在了當緩存中已經請求過數據的處理邏輯中。若是咱們當即調用it.next(...)
,這樣會發生一個error,這是由於generator尚未完成paused操做。咱們的函數首先要徹底調用request(..)
,這時纔會啓動yield的暫停。所以,咱們不能當即在request(..)
中當即調用it.next(...)
,這是由於這時generator仍然在運行(yield並無執行)。可是咱們能夠稍後一點調用it.next(...)
,等待如今的線程執行完畢,這就是setTimeout(..0)
這句有魔性的代碼放在這裏的意義。咱們稍後還會有一個更好的解決辦法。
如今,咱們的generator代碼並不須要發生任何變化:
var restult1 = yield request('http://some.url.1'); var data = JSON.parse(result1); ...
看到沒?咱們的generator邏輯(也就是咱們的流程邏輯)即便增長了緩存處理功能後,仍不須要發生任何改變。
*main()
中的代碼仍是隻須要請求數據後暫停,以後等到數據返回後順序執行下去。在咱們當前的狀況下,‘暫停’可能相對要長一些(作一個服務器的請求,大約要300~800ms),或者他能夠幾乎當即返回(走setTimeout的邏輯),可是咱們的流程邏輯徹底不須要關心這些。
這就是將異步編程抽象成更小細節的真正力量。
上面的方法能夠適用於那些比較簡單的異步generator工做流程。可是它將很快收到限制,所以咱們須要一些更強大的異步機制與咱們的generator來合做,這樣才能夠發揮出更強大的功能。那是什麼機制:Promise。
早先的Ajax實例老是會收到嵌套回調的困擾,問題以下:
1.沒有明確的方法來處理請求error。咱們都知道,Ajax請求有時是會失敗的,這時咱們須要使用generator中的it.throw(...)
,同時還須要使用try...catch
來處理請求錯誤時的邏輯。可是這更可能是一些在後臺(咱們那些在iterator中的代碼)手動的工做。我須要一些能夠服用的方法,放在咱們本身代碼的generator中。
2.假如makeAjaxCall(..)
這段代碼不在咱們的控制下了,或者他須要屢次調用回調,又或者它同時返回success與error,等等。這時咱們的generator的會變得亂七八糟(返回error實現,出現異常值,等等)。控制以及防止發生這類問題是須要花費大量的手工時間的,並且一點也不能即插即用。
3.一般我須要執行並行執行任務(好比同時作2個Ajax請求)。因爲generator yield機制都是逐步暫停,沒法在同時運行另外一個或多個任務,他的任務必須一個一個的按順序執行。所以,這不是太容易在一個generator中去操做多任務,咱們只能默默的在背後手擼大量的代碼。
就像你看到的,全部的問題都被解決了。可是沒人願意每次都去反覆的去實現一遍這些方法。咱們須要一種更強大的模式,專門設計出一個可信賴的,可重用的基於generator異步編程的解決方法。
什麼模式?把promise與yield結合,使得能夠在執行完成後恢復generator的流程。
讓咱們稍微用promise修改下request(..)
,讓yield返回一個promise。
function request(url) { //如今返回一個promise了 return new Promise( function(resolve, reject) { makeAjaxCall(url, resolve); }); }
request(..)
如今由一個promise構成,當Ajax請求完成後會返回這個promise,可是而後呢?
咱們須要控制generator的iterator,它將接受到yield返回的那個promise,同時經過next(...)恢復generator運行,並把他們傳遞下去,我增長了一個runGenerator(...)
方法來作這件事。
//比較簡單,沒有error事件處理 funtion runGenerator(g) { var it = g(), retl //異步iterator遍歷generator (function iterate(val) { //返回一個promise ret = it.next(val); if(!ret.done) { if('then' in ret.value) { //等待接收promise ret.value.then(iterate); } //獲取當即就有的數據,不是promise了 else { //避免同步操做 setTimeout(function() { iterate(ret.value); }, 0); } } })(); }
關鍵點 :
自動初始化generator(直接建立它的iterator),而且異步遞將他一直運行到結束(當done:true
就不在執行)
若是Promise被返回出來,這時咱們就等待到執行then(...)
方法的時候再處理。
若是是能夠當即返回的數據,咱們直接把數據返回給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方法不須要有什麼變化,由於咱們把那些邏輯都從咱們的流程管理中分離出去了。
儘管如今的yield是返回一個promise了,並把這個promise傳遞給下一個it.next(..)
,可是result1 = yield request(..)
這句獲得的值跟之前仍是同樣的。
咱們如今開始用promise來管理generator中的異步代碼,這樣咱們就解決掉全部使用回調函數方法中會出現的反轉/信任問題。因爲咱們用了generator+promise的方法,咱們不須要增長任何邏輯就解決掉了以上全部問題
咱們能夠很容易的增長一個error異常處理。雖然不在runGenerator(...)
中,可是很容易從一個promise中監聽error,並它他們的邏輯寫在it.throw(..)
裏面,這時咱們就能夠用上try..catch
`方法在咱們的generator代碼中去獲取和管理erros了。
咱們獲得到全部的 control/trustability,徹底不須要增長代碼。
promise有着很強的抽象性,讓咱們能夠實現一些多任務的並行操做。
好比:`Promise.all([ .. ])`就能夠並行執行一個promise數組,yield雖然只能拿到一個promise,可是這個是全部子promise執行完畢以後的集合數組。
先讓咱們看下error處理的代碼:
function request(url) { return new Promise( function(resolve, reject) { //第一個參數是error 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); retrun; } var data = JSON.parse(result1); try{ var result2 = yield request('http://some.url.2?id='+data.id); } catch(err) { console.log('Error:' + err); retrun; } var resp = JSON.parse(result2); console.log("The value you asked for: " + resp.value); });
若是執行url的fetch時promise被reject(請求失敗,或者異常)了,promise會給generator拋出一個異常,經過try..catch
語句能夠獲取到了。
如今,咱們讓promise來處理更復雜的異步操做:
function request(url) { return new Promise( function(resolve, reject) { makeAjax(url, resolve); }) //獲取到返回的text值後,作一些處理。 .then( function(text) { //若是咱們拿到的是一個url就把text提早出來在返回 if(/^http?:\/\/.+/.text(text)) { return request(text); } //若是咱們就是要一個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([ .. ]) 裏面放了3個子promise,主promise完成後就會在runGenerator中恢復generator。子promise拿到的是一天重定向的url,咱們會把它丟給下一個request請求,而後獲取到最終數據。
任何複雜的異步功能均可以被promise搞定,並且你還能夠用generator把這些流程寫的像同步代碼同樣。只要你讓yield返回一個promise。
如今能夠稍微提下ES7了,它更像把runGenerator(..)
這個異步執行邏輯作了一層封裝。
async funtion 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();
咱們直接調用main()
就能夠執行完全部的流程,不須要調用next
,也不須要去實現runGenerator(..)
之類的來管理promise邏輯。只須要把yield關鍵詞換成await就能夠告訴異步方法,咱們在這裏須要等到一個promise後纔會接着執行。
有了這些原生的語法支持,是否是很酷。
generator + yielded promise(s)
的組合目前是最強大,也是最優雅的異步流程管理編程方式。經過封裝一層流執行邏輯,咱們能夠自動的讓咱們的generator執行結束,而且還能夠像處理同步邏輯同樣管理error事件。
在ES7中,咱們甚至連這一層封裝都不須要寫了,變得更方便