從Generator開始學習Koa

Koa是最近比較火的一款基於Node的web開發框架。說他是一個框架,其實他更像是一個函數庫,經過某種思想(或者說某種約定),將衆多的中間件聯繫在一塊兒,從而提供你所須要的web服務。node

Koa作了兩件很重要的事:web

  1. 封裝node的request和response對象到Context上,還提供了一些開發web應用以及api經常使用的方法api

  2. 提供了一套流程控制方式,將衆多中間件級聯在一塊兒數組

而我如今想討論的就是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?

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}

這個返回值有兩個屬性:valuedone,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 function?

瞭解了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,返回第一個中間件剛剛中斷的位置。

是否是很神奇?這些中間件就像洋蔥同樣,一層一層的深刻進去,又一層一層的走出來。

clipboard.png

那麼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的源碼,本身實現一下應該也不會太難。

理解了洋蔥模型,就不難明白,yieldPromise在其中所起的做用了。

關於Koa

關於Koa,還有太多值得拿出來討論的話題,我如今只是對Koa1.x中對Generator的使用作了一次整理,別的話題就慢慢再討論吧。

最後,若是你有什麼建議,歡迎不吝賜教~

相關文章
相關標籤/搜索