摘要:需要說明的是,每次執行完函數之后,都會返回一個對象這個返回值有兩個屬性和,對象通過這個返回值來告訴外界函數的執行情況。函數的返回值變成這樣可以發現的值變為了,因為函數已經執行完了。在規范中,新增了兩個協議可迭代協議和迭代器協議。
Koa是最近比較火的一款基于Node的web開發框架。說他是一個框架,其實他更像是一個函數庫,通過某種思想(或者說某種約定),將眾多的中間件聯系在一起,從而提供你所需要的web服務。
Koa做了兩件很重要的事:
封裝node的request和response對象到Context上,還提供了一些開發web應用以及api常用的方法
提供了一套流程控制方式,將眾多中間件級聯在一起
而我現在想討論的就是Koa的這套流程控制的思想。
先看一段從官方文檔上搬下來的代碼:
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中,我們約定所有的中間件都是以這種方式聲明的,如果你了解ES6,那你一定見過這種聲明方式。沒錯,這就是ES6中的generator function。Koa中,真正的中間件其實就是一個generator對象。
什么是Generator?Generator是ES6新引進的一個概念,使用Generator可以將函數的控制權交給函數外部。也就是說,你可以控制函數的執行進程。
舉個例子:
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 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,返回第一個中間件剛剛中斷的位置。
是不是很神奇?這些中間件就像洋蔥一樣,一層一層的深入進去,又一層一層的走出來。
那么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關于Koa,還有太多值得拿出來討論的話題,我現在只是對Koa1.x中對Generator的使用做了一次整理,別的話題就慢慢再討論吧。
最后,如果你有什么建議,歡迎不吝賜教~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/88264.html
摘要:到此為止,我們就基本講清楚了中的中間件洋蔥模型是如何自動執行的。 koa被認為是第二代web后端開發框架,相比于前代express而言,其最大的特色無疑就是解決了回調金字塔的問題,讓異步的寫法更加的簡潔。在使用koa的過程中,其實一直比較好奇koa內部的實現機理。最近終于有空,比較深入的研究了一下koa一些原理,在這里會寫一系列文章來記錄一下我的學習心得和理解。 在我看來,koa最核心...
摘要:返回后,代表操作已完成,記錄結束時間并輸出。從零組裝因為對的學習和使用,知道了自己對于后臺框架的真實需求。所以這回決定不用之內的工具,而是自己從零開始,組裝一個適合自己的框架。就是去和上,尋找一個一個的包并組裝在一起了而已。 起因 作為一個前端,Node.js算是必備知識之一。同時因為自己需要做一些后臺性的工作,或者完成一個小型應用。所以學習了Node的Express框架,用于輔助和加...
摘要:現在我們從實現一個簡易的方法開始探索其中的機制。其中內部的可以將上一個的返回值傳遞給外部。一言以蔽之實現了遞歸調用的方法。當執行到的中間件沒有時并且返回的為時逆序執行。 本文發布在github.com/ssssyoki,歡迎star,issues共同交流。 Koa是基于Node.js的下一代web開發框架,相比Express更輕,源碼只有幾百行。與傳統的中間件不同,在Koa 1.x中采...
摘要:本筆記共四篇源碼閱讀筆記源碼閱讀筆記源碼閱讀筆記服務器啟動與請求處理源碼閱讀筆記對象起因前兩天閱讀了的基礎,和中間件的基礎。的前端樂園原文鏈接源碼閱讀筆記服務器啟動與請求處理 本筆記共四篇Koa源碼閱讀筆記(1) -- coKoa源碼閱讀筆記(2) -- composeKoa源碼閱讀筆記(3) -- 服務器の啟動與請求處理Koa源碼閱讀筆記(4) -- ctx對象 起因 前兩天閱讀了K...
閱讀 1321·2021-11-16 11:45
閱讀 2241·2021-11-02 14:40
閱讀 3882·2021-09-24 10:25
閱讀 3032·2019-08-30 12:45
閱讀 1261·2019-08-29 18:39
閱讀 2476·2019-08-29 12:32
閱讀 1607·2019-08-26 10:45
閱讀 1923·2019-08-23 17:01