上一篇分析了co3.x版本的原理,因爲co從4.0採用es6的標準promise來實現,簡要介紹下:html
( 須要你對 promise 有必定的瞭解 )node
在這裏,yield的返回對象從thunk方法變成promise對象,因爲它們都接受方法做爲參數,這樣generator便能經過這個回調方法來控制,持續迭代下去,這裏給個示範代碼:es6
function core(genfunc) { var g = genfunc(); var next = function (res) { // 如今res.value是promise,co內部對它其餘數據類型包裝成promise res.value.then(function (res) { next(gen.next(res)); }, function () { gen.throw(err); }); }; next(gen.next()); }
這裏,每次生成器返回的對象變成了promise,而後咱們在promise的resolve方法中遞歸調用next方法,這樣生成器就能夠持續迭代下去了. (這裏沒加終止判斷和異常處理)數據庫
因爲以前的版本中,有不少使用了thunk的包裝方法,爲了保持兼容,co中對此作了判斷,若是res.value不是promise,而是thunk,它會作兼容處理,對此咱們不用修改以前的代碼,這裏是示範:promise
function thunkToPromise(fn) { return new Promise(function (resolve, reject) { fn(function(err, res) { resolve(res); }); }); }
對於co支持的其餘數據類型的封裝,我就不介紹了,感興趣的能夠去看co的源碼。app
好了,長篇大論鋪墊了這麼久,該說說koa了,咱們一般使用koa的時候都是經過use添加一個genfunc,因此先看看use作了什麼:koa
app.use = function(fn){ this.middleware.push(fn); return this; };
它什麼也沒作,只是將參數保存起來,而後返回引用,以便支持鏈式調用,接下來咱們看看koa的啓動:oop
app.listen = function(){ var server = http.createServer(this.callback()); return server.listen.apply(server, arguments); };
這個和node的官方http示範很像,沒什麼要解釋的,再看看callback方法:this
app.callback = function(){ var mw = [respond].concat(this.middleware); var gen = compose(mw); var fn = co.wrap(gen); var self = this; if (!this.listeners('error').length) this.on('error', this.onerror); return function(req, res){ res.statusCode = 404; var ctx = self.createContext(req, res); onFinished(res, ctx.onerror); fn.call(ctx).catch(ctx.onerror); } };
這裏,在調用co以前,它採用compose方法對以前咱們註冊的回調作了一次處理,compose是koa-compose包中的方法,這是源碼:code
function compose(middleware){ return function *(next){ var i = middleware.length; var prev = next || noop(); var curr; while (i--) { curr = middleware[i]; prev = curr.call(this, prev); } yield *prev; } }
這裏是koa控制流的核心,其實代碼很簡單,可是須要細心分析下
1.while循環是逆序的,也就是說最後一個genfunc接收的參數是noop, 源碼是:
function *noop(){}
2.從倒數第二個genfunc開始,每個方法接受的參數都是緊挨它下一個genfunc的實例,也就是咱們在koa方法中引用的形參: next,這樣便能經過在方法中使用yield next來控制流轉過程。
3.compose方法返回的也是一個generator,這樣co便能迭代它,可是它每次產生的數據並非promise,而是咱們註冊的genfunc的實例,因此這裏它使用了yield *prev,用來轉移迭代對象,這樣每次迭代的就是咱們註冊是生成器了。
4.若是你看到這裏你看懂了,就會有個疑問:爲何在咱們註冊的生成器中,咱們使用的是yield next,而不是yield *next。其實你怎麼寫均可以,co內部對res.value作了類型判斷,若是是generator,它本身會遞歸調用co(generator),而co也會返回一個promise。(我寫的core方法沒有對此進行描述)
這裏說明下 yield generator 和yield *generator的區別:前者會將一個generator做爲返回值,後者則會將控制交給這個generator,並迭代它,詳細介紹能夠看 MDN-function *。
寫到這裏,koa的實現方法和流轉控制基本就清楚了,這裏作一個總結:
1.在koa註冊的genfunc中,能夠經過yield next 來控制程序的流轉,若是不當心忘記了作這個事情,那麼經過上面的代碼能夠發現,程序會捨棄後面的generator,提早返回。
2.整個控制流實際像一個洋蔥,先進的必定會後出,後進入的必定會先出,可是若是咱們故意跳開某一步的next調用,那麼這個洋蔥就不會被從中間穿過,而是穿過部分外層,而後逆序在穿過這些外層,程序就執行完了,不會進入後面註冊的那些genfunc。
3.因爲上面的緣由,這裏有一個陷阱:若是你使用了相似router之類的組件,一般來講通常若是匹配了某個指定的path,這個router就會執行,而後它後面的genfunc會被捨棄,它會提早返回。而若是你要配置一些全局的處理,譬如壓縮html,關閉數據庫鏈接這類必須等到最後執行的操做,則必須在這些有可能會中斷控制流的方法以前註冊,而後經過yield next直接轉移給下一步便可,否則有可能會被提早返回不被執行。
打完收工,歡迎批評指正。