Koa是最近比較火的一款基於Node的web開發框架。說他是一個框架,其實他更像是一個函數庫,經過某種思想(或者說某種約定),將衆多的中間件聯繫在一塊兒,從而提供你所須要的web服務。node
Koa作了兩件很重要的事:web
封裝node的request和response對象到Context上,還提供了一些開發web應用以及api經常使用的方法api
提供了一套流程控制方式,將衆多中間件級聯在一塊兒數組
而我如今想討論的就是Koa的這套流程控制的思想。app
先看一段從官方文檔上搬下來的代碼:框架
var koa = require('koa'); var app = koa(); // x-response-time app.use(function *(next){ var start = new Date; yield next; var ms = new Date - start; this.set('X-Response-Time', ms + 'ms'); }); // logger app.use(function *(next){ var start = new Date; yield next; var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); }); // response app.use(function *(){ this.body = 'Hello World'; }); app.listen(3000);
app是Koa的一個實例,經過調用app.use,向Koa內部維護的一個middlewares數組中,添加中間件。而咱們所說的中間件,其實就是那個做爲app.use參數的,使用奇怪方式聲明的function。koa
在Koa中,咱們約定全部的中間件都是以這種方式聲明的,若是你瞭解ES6,那你必定見過這種聲明方式。沒錯,這就是ES6中的generator function。Koa中,真正的中間件其實就是一個generator對象。函數
Generator是ES6新引進的一個概念,使用Generator能夠將函數的控制權交給函數外部。也就是說,你能夠控制函數的執行進程。oop
舉個例子:ui
function *sayHello(){ console.log("before say"); yield console.log("hello!"); console.log("end say"); } var a = sayHello(); a.next(); // 輸出before say 輸出hello! a.next(); // 輸出end say
首先咱們定義了一個叫作sayHello的generator function,它跟普通的function不一樣,執行sayHello(),並不會執行函數體內部的程序,可是會返回一個generator對象。所以a的值實際上長這樣:
sayHello {[[GeneratorStatus]]: "suspended"}
對generator function來講,執行函數只是生成了一個generator對象,不會執行函數的內在邏輯,而使用者卻能夠經過這個generator對象來達到控制函數執行的目的。就好比說這個sayHello函數,我能夠在須要的時候,執行a.next()方法,來執行函數的內部邏輯。第一次執行a.next(),函數開始執行,直到它遇到yield指令,它會執行yield以後的表達式,並返回一個值,而後中斷函數的運行。所以,咱們看到,第一次執行a.next()後,函數輸出了"before say"和"hello!"。須要說明的是,每次執行完next函數以後,都會返回一個對象:
Object {value: undefined, done: false}
這個返回值有兩個屬性:value和done,generator對象經過這個返回值來告訴外界函數的執行狀況。value的值是yield以後的表達式的值,done則是函數執行的狀態,若是函數未執行完,則其值爲false,不然是true。在sayHello中,yield以後是console語句,所以返回的對象中value爲undefined。
這個時候,咱們再次調用a.next(),程序輸出"end say"。next函數的返回值變成這樣:
Object {value: undefined, done: true}
能夠發現done的值變爲了true,由於函數已經執行完了。
Generator能夠被用來做迭代器。
首先了解一下迭代器。在ES6規範中,新增了兩個協議:可迭代協議和迭代器協議。在迭代器協議中指明,一個實現了next方法而且該方法的返回值有done和value兩個屬性的對象,能夠被當作迭代器。這些要求正好符合咱們的Generator對象。舉一個被當作迭代器使用的例子:
function *range(start, end){ for (let i = start; i < end; i++) { yield i; } } var a = range(0, 10); // 輸出0...9 for (let i of a) { console.log(i); }
其實道理是同樣的,Generator把程序的控制權交給了外部,哪裏調用next,程序就在哪裏執行。可想而知for...of的實現原理也必定是在內部循環執行了next方法,直到返回值的done屬性變成true才中止。
瞭解了Generator,回頭再去看那段官方文檔上搬來的代碼。
var koa = require('koa'); var app = koa(); // x-response-time app.use(function *(next){ var start = new Date; yield next; var ms = new Date - start; this.set('X-Response-Time', ms + 'ms'); }); // logger app.use(function *(next){ var start = new Date; yield next; var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); }); // response app.use(function *(){ this.body = 'Hello World'; }); app.listen(3000);
咱們來分析代碼。app.use將一個個中間件放入middlewares數組中,而app.listen啓動了一個3000端口來監聽http服務。實際上app.listen這個方法,底層是這樣實現的:
var http = require('http'); var koa = require('koa'); var app = koa(); http.createServer(app.callback()).listen(3000);
這樣你就明白了,當請求來臨時,會觸發在createServer時註冊的回調函數(app.callback()的返回值),這個回調函數的執行其實就引起了一連串的中間件的執行。
先說結果,在探索原理。
middlewares數組中的這些中間件順序執行,先開始進入第一個中間件 —— x-response-time,遇到yield中斷執行,轉而進入第二個中間件 —— logger,一樣遇到yield中斷執行,進入第三個中間件 —— response,此次沒有遇到yield,第三個中間件執行完畢,頁面輸出"Hello World",done的值變爲true。這個時候,再返回去執行第二個中間件剛剛中斷的地方,直到第二個中間件的done也變爲true,返回第一個中間件剛剛中斷的位置。
是否是很神奇?這些中間件就像洋蔥同樣,一層一層的深刻進去,又一層一層的走出來。
那麼Koa是如何實現這般神奇的流程控制的呢?
Koa內部依賴了一個叫co的流程控制庫。
首先,Koa實現了一個叫Koa-compose的中間件,這個中間件用來將middlewares中的全部中間件串聯起來。其實現代碼以下:
/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */ function compose(middleware){ return function *(next){ if (!next) next = noop(); var i = middleware.length; while (i--) { next = middleware[i].call(this, next); } return yield *next; } } /** * Noop. * * @api private */ function *noop(){}
compose函數會返回一個能將衆多中間件串聯起來的Generator函數。這個函數從最後一箇中間件開始執行,將生成的Generator對象扔給它的上一個中間件,依次類推,直到第一個中間件。這個結構真的很像一顆洋蔥,從最後一箇中間件開始,一層一層往上面包。
這樣生成一個Generator對象以後,Koa把它交給了co這個流程控制庫。co實際上是個很抽象的東西。爲了理解它的原理,咱們能夠先思考一下,若是把這個Generator對象交給咱們,咱們怎麼相似於實現剛剛那個圖所展現的效果?
從洋蔥的最外層皮開始往裏剝。執行第一次.next()函數,第一層中間件yield以前的程序執行完畢,經過yield next,咱們拿到了第二層中間件的Generator對象。這個時候怎麼辦呢?按照剛剛那幅圖,第一層中間件,必需要等到第二層中間件的done狀態變爲true以後,才能夠繼續執行以後的程序,即只有在第二層中間件的done狀態變爲true以後,才能再次執行第一層中間件Generator對象的.next()函數。一樣的,以後全部的中間件都要重複這樣的過程,第一層等待第二層,第二層等待第三層......那麼當狀態改變的時候,是否是應該有我的來通知咱們?對,這個時候Promise就該出場了。
co將每一箇中間件.next()的運行結果的value屬性都封裝成一個Promise,在其done狀態變爲true時,resolve()這個Promise,對於洋蔥裏面的部分,每一層resolve以後,都會觸發上一層中間件的.next()函數,並檢查其狀態。直到洋蔥的最外面一層也resolve了,控制權就交還給Koa,而Koa會在這個時候,發起response。
co的大致思想就是這樣,若是想繼續深刻,能夠去看co的源碼,本身實現一下應該也不會太難。
理解了洋蔥模型,就不難明白,yield和Promise在其中所起的做用了。
關於Koa,還有太多值得拿出來討論的話題,我如今只是對Koa1.x中對Generator的使用作了一次整理,別的話題就慢慢再討論吧。
最後,若是你有什麼建議,歡迎不吝賜教~