koa是基于node.js的下一代web開發架構,相比express更輕,源碼隻有幾百行。與傳統的中間件不同,在koa 1.x中采用了generator實作中間件,這需要開發者熟悉es6中的generator,promise相關知識。
在koa官方文檔示例代碼中,采用yield next為跳轉信号,然後會逆序執行中間件剩下的代碼邏輯。這其中的邏輯非常有趣,本文将對其進行深入的分析。
section a:
koa的中間件跑在co子產品下,而co可以将異步“變為”同步,進而實作用同步的方法寫異步代碼,避免了node.js大量的回調嵌套。現在我們從實作一個簡易的co方法開始探索其中的機制。
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相關知識,接下來我們逐漸分析這段代碼:
1.我們首先定義一個參數為generator的co函數。
2.當傳入generator後(即 app.use(function *(){...}) )定義 next
方法實作對generator(可以了解為狀态機)的狀态周遊,由于每次周遊器指向新的 yield ,傳回結構如
{value:'promise','done':'true/false'} 的值,當 done 的值為 false 時周遊狀态完畢并傳回,若為
true 則繼續周遊。其中内部的 g.next(data) 可以将上一個 yield 的傳回值傳遞給外部。
3.同時,若generator中含有多個 yield 且周遊未完成(即 result.value 是 promise 對象
&& result.done === false ), resolve() 所傳遞的資料可以在接下來 then()
方法中直接使用,即遞歸調用,直到 result.done === true 周遊結束并退出。
這裡可能存在一個疑惑,在第一次調用 next() 方法時data為 undefined ,那是否會導緻error産生呢?其實v8引擎在執行時,會自動忽略第一次調用 next() 時的參數,是以隻有從第二次使用 next() 方法時參數才是有效的。
一言以蔽之,co實作了promise遞歸調用generator的next方法。
section b:
了解了co的運作原理後,再來了解middleware的機制就容易多了。
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}
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,代碼隻有短短幾十行,但組合在一起卻非常精巧奇妙,值得細細品味。
作者:佚名
來源:51cto