編者按:看完本文,你能對ES6的Generator有一個很好的理解,輕鬆地以同步的方式寫異步代碼,也能初步理解到TJ大神的co框架的原理。javascript
前言:ES6在2015年6月正式發佈,它帶給js帶來許多新特性,其中一個就是Generator,雖然其它語言如python早就有了,但js的Generator和它們的仍是有點不同的,js的Generator重點在解決異步回調金字塔問題,巧妙的使用它能夠寫出看起來同步的代碼。html
咱們都知道js跟其它語言相比,最大的特性就是異步,因此當咱們要取異步取一個文件內容時,通常咱們會這樣寫java
$.get('http://youzan.com/test.txt', function(data){ console.log(data); })
若是取完A文件後又要再取B文件:node
$.get('http://youzan.com/A.txt', function(a){ $.get('http://youzan.com/B.txt', function(b){ console.log(b); } }
再取一個C文件可能這樣寫:python
$.get('http://youzan.com/A.txt', function(a){ $.get('http://youzan.com/B.txt', function(b){ $.get('http://youzan.com/C.txt', function(c){ console.log(c); } } }
當有更多的異步操做,業務邏輯更爲的複雜時,按上面的寫法維護時心中確定要罵娘了。那有沒有更好一點的寫法呢?在Generator出來以前可使用promise實現,雖說promise也是es6的一部分,es6標準未出以前已經有不少ployfill出來了。git
$.get('http://youzan.com/A.txt') .done(function(a){ return $.get('http://youzan.com/B.txt'); }) .done(function(b){ return $.get('http://youzan.com/C.txt'); }) .done(function(c){ console.log(c); })
promise的實現要比上面嵌套回調要優雅許多,但也能夠一眼看出異步回調的身影。目前js有不少框架要致力解決js金字塔回調,讓異步代碼書寫的邏輯更爲清晰,如async, wind.js, promise, deffer。這些框架都有本身的一些約定,如async是以數組形式來寫,promise是以回調參數方式,但它們都不能作到像寫c或java那樣第一行open一個file,而後第二行立刻讀取,來看看最新的Generator是怎麼作的:es6
co(function* (){ var a = yield $.get('http://youzan.com/A.txt'); var b = yield $.get('http://youzan.com/B.txt'); var c = yield $.get('http://youzan.com/C.txt'); console.log(c); })
上面代碼使用了co框架包裹,裏面一個Generator,從書寫上看它已經和其它同步語言差很少。寫了多年的異步看到上面代碼是否是感受不可思義呢?這就是Generator帶來的可喜之處,其實es6還更多的新東西等着你發現。下面來了詳細瞭解一下Generator。github
Generator是生成器的意思,它是一種能夠從中退出並在以後從新進入的函數。生成器的環境(綁定的變量)會在每次執行後被保存,下次進入時可繼續使用。生成器其實在其它語言很早就有了,好比python、c#,但與python不一樣的是js的generator更多的是提供一種異步解決方案。chrome
Generator使用function*
來定義,內部有yield
關鍵字,next
方法控制內部執行流程,每執行到一個yield
語句就會中斷,並返回一個迭代值,下次執行時從yield
的下一個語句繼續執行。一個生成器只能執行一次。express
一個簡單的定義以下:
function* hello() { var a = 'b' yield 'a'; return a; } var gen = hello(); console.log(gen); // => hello {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: undefined} console.log(gen.next()) // => {value: "a", done: false} console.log(gen.next()) // => {value: "b", done: true}
上面代碼經過調用hello()
,產生了一個生成器,內部代碼沒有執行。調用next
方法執行到yield
後暫停,內部環境被保存,next
執行返回一個對象,value
爲yield
的執行結果,done
表示迭代器是否完成。當迭代器完成後,done
爲true,value
爲return的值,繼續執行next
,value
將爲undefined
回到開頭的例子,Generator給咱們提供了直觀的寫法來處理異步回調,它讓代碼邏輯很是清晰。來了解一下Generator內部的一些原理
function* hello() { yield 'a'; return 'b'; } var gen = hello(); console.log(gen); // => hello {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: undefined}
使用chrome工具查看對象內容,可發現裏面有next
和throw
方法。
當咱們執行next
方法時,發現其返回了一個對象:
對象含有value
和done
兩個字段,value
爲yield 'a'的返回值,即爲'a',done
表示generator的狀態,爲true時表示執行完成。再執行next方法時,能夠看到done的值已經爲true了,並且value的值爲return的值。
查看gen自身內部的狀態,能夠看到GeneratorStatus已經爲closed了
綜上能夠得出Generator是經過調用next
方法來控制執行流程,當遇到yield語句時暫停執行。next
方法返回一個對像{value: 'yield', done: false},value存儲的yield 執行結果,done表示迭代器是否執行完成。
經過上面的瞭解貌似generator並無太大卵用,不能如所說的用同步情懷書寫異步代碼。上面漏了很重要的一點就是yield的返回值及next的參數。看下面一段代碼:
function* hello() { var ret = yield 'a'; console.log(ret); }
而後過程以下一下:
從上面執行過程能夠看到,ret與第二個next的參數值同樣,這是Generator的傳值方式。yield的返回值就是next的參數,第一個next因爲執行到yield語句以前就暫停了,因此參數b沒有用。
這裏也提一下上面出現的throw方法。
在generator中使用gen.throw('error')來拋出異常。當出現異常後,迭代停止,再次執行gen.next()時,將返回{value: undefined, done: true};
使用try catch可捕獲gen.throw出來的異常。
至此對generator瞭解的也差很少了,但貌似使用它來寫代碼感受挺變扭的,由於你要不停的next,若是有一個函數能自動執行generator函數就行了。就像以前提到的代碼:
co(function* (){ var a = yield $.get('http://youzan.com/A.txt'); var b = yield $.get('http://youzan.com/B.txt'); var c = yield $.get('http://youzan.com/C.txt'); console.log(c); })
上面提到的Generator內部原理能夠總結出,right
這邊執行後的結果放到value裏,next的參數放到了left
這邊。爲了讓right
這邊執行後的結果放到left
,那right
就得返回一個function,傳一個callback進行,而後在callback裏執行next方法。經過了角Generator的數據傳遞過程就能夠寫出一個簡易版的co來自動執行next方法,以達到上面代碼效果:
function co(genFunc) { var gen = genFunc(); var next = function(value){ var ret = gen.next(value); if (!ret.done) { ret.value(next); } } next(); } function getAFromServer(url){ /* *do something sync */ return function(cb) { /* *do something async */ var a = 'data A from server'; cb(a); // 返回讀到的內容 } } function getBFromServer(url){ /* *do something sync */ return function(cb) { /* *do something async */ var b = 'data B from server'; cb(b); // 返回讀到的內容 } } co(function* (){ var ret = yield getFromSever('url of A'); console.log(ret); // 輸出 data A from server var retB = yield getFromSever('url of B'); console.log(retB); // 輸出 data B from server })
上面的co就是一個很是簡單的自動執行generator next的函數,且right
這邊的值能正確傳到left
,惟一的要求是getA的寫法必須trunk的寫法。像咱們使用nodejs的一些異步api,可以使用trunkify來轉成trunk形式。
在co內部主要靠next來實現循環,靠外部cb()來驅動運行。大致流程以下:
瞭解了co原理,那就能夠把它作得更強大一些,如支持Promise,支持nodejs寫法的異常處理。這個能夠參考co, trunks的代碼。
根據這個ECMAScript 6 compatibility table的資料顯示,目前已經有以下平臺能夠支持:
Chrome 35+ (about://flags中開啓)
Firefox 31+ (默認開啓)
nodejs harmony
nodejs 0.11+
https://developer.mozilla.org...*
http://es6.ruanyifeng.com/#do...
http://kangax.github.io/compa...
http://www.slideshare.net/Ram...
本文首發於有贊技術博客: http://tech.youzan.com/es6-ge...