Koa是基於Node.js的下一代web開發框架,相比Express更輕,源碼只有幾百行。與傳統的中間件不一樣,在Koa 1.x中採用了generator實現中間件,這須要開發者熟悉ES6中的generator,Promise相關知識。javascript
在Koa官方文檔示例代碼中,採用yield next爲跳轉信號,而後會逆序執行中間件剩下的代碼邏輯。這其中的邏輯很是有趣,本文將對其進行深刻的分析。 css
Koa的中間件跑在co模塊下,而co能夠將異步「變爲」同步,從而實現用同步的方法寫異步代碼,避免了Node.js大量的回調嵌套。如今咱們從實現一個簡易的co方法開始探索其中的機制。java
function co(generator){
let g = generator();
let next = function(data){
let result = g.next(data);
if(result.done){
return ;
};
if(result.value instanceof Promise){
result.value.then(function(d){
next(d);
},function(err){
next(err);
});
}else{
next();
};
};
next();
};複製代碼
首先須要瞭解generator相關知識,接下來咱們逐步分析這段代碼: es6
1.咱們首先定義一個參數爲generator的co函數。web
2.當傳入generator後(即app.use(function *(){...})
)定義next
方法實現對generator(能夠理解爲狀態機)的狀態遍歷,因爲每次遍歷器指向新的yield
,返回結構如{value:'Promise','done':'true/false'}
的值,當done
的值爲false
時遍歷狀態完畢並返回,若爲true
則繼續遍歷。其中內部的g.next(data)
能夠將上一個yield
的返回值傳遞給外部。api
3.同時,若generator中含有多個yield
且遍歷未完成(即result.value
是Promise
對象 && result.done === false
),resolve()
所傳遞的數據能夠在接下來then()
方法中直接使用,即遞歸調用,直到result.done === true
遍歷結束並退出。數組
這裏可能存在一個疑惑,在第一次調用next()
方法時data爲undefined
,那是否會致使error產生呢?其實V8引擎在執行時,會自動忽略第一次調用next()
時的參數,因此只有從第二次使用next()
方法時參數纔是有效的。promise
理解了co的運行原理後,再來理解middleware的機制就容易多了。app
middleware實現了所謂「逆序」執行,其實就是每次調用use()
方法時,將generator存入數組(記爲s)中保存。框架
在執行的時候先定義一個執行索引(記爲index)和跳轉標記(記爲turn,也就是yield next
中的next
),再定義一個保存generator函數對象的數組(記爲gs)。而後獲取當前中間件generator,接着獲取該generator的函數對象,將函數對象放在gs數組內保存,再執行generator的next()
方法。
執行開始後,根據返回的value
進行不一樣的處理,若是是標記turn(即執行到了yield next
),說明該跳到下一個中間件了,此時令index++
,而後從數組g中獲取下一個中間件重複上一個中間件的執行流程。
當執行到的中間件沒有yield
時,而且返回的done
爲true
時,逆序執行。今後前用於保存generator函數對象的gs數組中取出上一個generator對象,而後執行generator的next()
方法,直到所有結束。
咱們打開Koa的application.js
文件:
/** * Use the given middleware 'fn'. * * @param {GeneratorFunction} fn * @return {Application} self * @api public */
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are not allowed,
// so we have to make sure that 'fn' is a generator function
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
};複製代碼
顯而易見,app.use()
方法就是將generator傳入this.middleware
數組中。其餘部分的邏輯源碼註釋很是清晰,再也不贅述。
咱們再打開Koa-compose模塊的index.js
文件:
/** * 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;
}
}複製代碼
其中最關鍵的就是while
語句。
將以前app.use()
傳入並存儲在middleware
中的generator逆序取出並執行,將每一個generator執行後的結果(即generator() === iterator)做爲參數傳入下一個(按數組的順序則爲前一個)generator中,在最後一個generator(數組第一個)執行後得出的next
變量(即第一個generator的iterator),執行yield *next
(即執行第一個generator的iterator)將所有generator像鏈表般串聯起來。
根據yield *
的特性,yield *next
將依次執行全部套用的next
(相似遞歸),從而造成所謂「正序執行再逆序執行」的流程。
從co到compose,代碼只有短短几十行,但組合在一塊兒卻很是精巧奇妙,值得細細品味。