摘要:拿到下一個中間件后把他交給去處理當中間件執行結束了,就把的狀態設置為成功。
前言
原文地址
用在前面最近幾天花了比較長的時間在koa(1)的源碼分析上面,初次看的時候,被中間件執行那段整的暈乎乎的,完全不知道所以,再次看,好像明白了些什么,再反復看,我去,簡直神了,簡直淚流滿面,簡直喪心病狂啊!!!
下面的例子會在控制臺中打印出一些信息(具體打印出什么?可以猜猜?),然后返回hello world。
let koa = require("koa") let app = koa() app.use(function * (next) { console.log("generate1----start") yield next console.log("generate1----end") }) app.use(function * (next) { console.log("generate2----start") yield next console.log("generate2----end") this.body = "hello world" }) app.listen(3000)
用過koa的同學都知道添加中間件的方式是使用koa實例的use方法,并傳入一個generator函數,這個generator函數可以接受一個next(這個next到底是啥?這里先不闡明,在后面會仔細說明)。
執行use干了嘛
這是koa的構造函數,為了沒有其他信息的干擾,我去除了一些暫時用不到的代碼,這里我們把目光聚焦在middleware這個數組即可。
function Application() { // xxx this.middleware = []; // 這個數組就是用來裝一個個中間件的 // xxx }
接下來我們要看use方法了
同樣去除了一些暫時不用的代碼,可以看到每次執行use方法,就把外面傳進來的generator函數push到middleware數組中
app.use = function(fn){ // xxx this.middleware.push(fn); // xxx };
好啦!你已經知道koa中是預先通過use方法,將請求可能會經過的中間件裝在了一個數組中。
接下來我們要開始本文的重點了,當一個請求到來的時候,是怎樣經過中間件,怎么跑起來的
首先我們只要知道下面這段callback函數就是請求到來的時候執行的回調即可(同樣盡量去除了我們不用的代碼)
app.callback = function(){ // xxx var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware)); // xxx return function(req, res){ // xxx fn.call(ctx).then(function () { respond.call(ctx); }).catch(ctx.onerror); // xxx } };
這段代碼可以分成兩個部分
請求前的中間件初始化處理部分
請求到來時的中間件運行部分
我們分部分來說一下
var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware));
這段代碼對experimental做了下判斷,如果設置為了true那么koa中將可以支持傳入async函數,否則就執行co.wrap(compose(this.middleware))。
只有一行初始化中間件就做完啦?
我知道koa很屌,但也別這么屌好不好,所以說評價一個好的程序員不是由代碼量決定的
我們來看下這段代碼到底有什么神奇的地方
compose(this.middleware)
把裝著中間件middleware的數組作為參數傳進了compose這個方法,那么compose做了什么事呢?其實就是把原本毫無關系的一個個中間件給首尾串起來了,于是他們之間就有了千絲萬縷的聯系。
function compose(middleware){ return function *(next){ // 第一次得到next是由于*noop生成的generator對象 if (!next) next = noop(); var i = middleware.length; // 從后往前開始執行middleware中的generator函數 while (i--) { // 把后一個中間件得到的generator對象傳給前一個作為第一個參數存在 next = middleware[i].call(this, next); } return yield *next; } } function *noop(){}
文字解釋一下就是,compose將中間件從最后一個開始處理,并一直往前直到第一個中間件。其中非常關鍵的就是將后一個中間件得到generator對象作為參數(這個參數就是文章開頭說到的next啦,也就是說next其實是一個generator對象)傳給前一個中間件。當然最后一個中間件的參數next是一個空的generator函數生成的對象。
我們自己來寫一個簡單的例子說明compose是如何將多個generator函數串聯起來的
function * gen1 (next) { yield "gen1" yield * next // 開始執行下一個中間件 yield "gen1-end" // 下一個中間件執行完成再繼續執行gen1中間件的邏輯 } function * gen2 (next) { yield "gen2" yield * next // 開始執行下一個中間件 yield "gen2-end" // 下一個中間件執行完成再繼續執行gen2中間件的邏輯 } function * gen3 (next) { yield "gen3" yield * next // 開始執行下一個中間件 yield "gen3-end" // 下一個中間件執行完成再繼續執行gen3中間件的邏輯 } function * noop () {} var middleware = [gen1, gen2, gen3] var len = middleware.length var next = noop() // 提供給最后一個中間件的參數 while(len--) { next = middleware[len].call(null, next) } function * letGo (next) { yield * next } var g = letGo(next) g.next() // {value: "gen1", done: false} g.next() // {value: "gen2", done: false} g.next() // {value: "gen3", done: false} g.next() // {value: "gen3-end", done: false} g.next() // {value: "gen2-end", done: false} g.next() // {value: "gen1-end", done: false} g.next() // {value: undefined, done: true}
看到了嗎?中間件被串起來之后執行的順序是
gen1 -> gen2 -> gen3 -> noop -> gen3 -> gen2 -> gen1
從而首尾相連,進而發生了關系?。
co.wrap通過compose處理后返回了一個generator函數。
co.wrap(compose(this.middleware))
所有上述代碼可以理解為
co.wrap(function * gen ())
好,我們再看看co.wrap做了什么,慢慢地一步步靠近了哦
co.wrap = function (fn) { createPromise.__generatorFunction__ = fn; return createPromise; function createPromise() { return co.call(this, fn.apply(this, arguments)); } }
可以看到co.wrap返回了一個普通函數createPromise,這個函數就是文章開頭的fn啦。
var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware));中間件開始跑起來啦
前面已經說完了,中間件是如何初始化的,即如果由不相干到關系密切了,接下來開始說請求到來時,初始化好的中間件是怎么跑的。
fn.call(ctx).then(function () { respond.call(ctx); }).catch(ctx.onerror);
這一段便是請求到來手即將要經過的中間件執行部分,fn執行之后返回的是一個Promise,koa通過注冊成功和失敗的回調函數來分別處理請求。
讓我們回到
co.wrap = function (fn) { // xxx function createPromise() { return co.call(this, fn.apply(this, arguments)); } }
createPromise里面的fn就是經過compose處理中間件后返回的一個generator函數,那么執行之后拿到的就是一個generator對象了,并把這個對象傳經經典的co里面啦。如果你需要對co的源碼了解歡迎查看昨天寫的走一步再走一步,揭開co的神秘面紗,好了,接下來就是看co里面如何處理這個被compose處理過的generator對象了
再回顧一下co
function co(gen) { var ctx = this; var args = slice.call(arguments, 1) // we wrap everything in a promise to avoid promise chaining, // which leads to memory leak errors. // see https://github.com/tj/co/issues/180 return new Promise(function(resolve, reject) { if (typeof gen === "function") gen = gen.apply(ctx, args); if (!gen || typeof gen.next !== "function") return resolve(gen); onFulfilled(); /** * @param {Mixed} res * @return {Promise} * @api private */ function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); } /** * @param {Error} err * @return {Promise} * @api private */ function onRejected(err) { var ret; try { ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } /** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */ function next(ret) { if (ret.done) return resolve(ret.value); var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected(new TypeError("You may only yield a function, promise, generator, array, or object, " + "but the following object was passed: "" + String(ret.value) + """)); } }); }
我們直接看一下onFulfilled,這個時候第一次進co的時候因為已經是generator對象所以會直接執行onFulfilled()
function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); }
而gen.next正是用于去執行中間件的業務邏輯,當遇到yield語句的時候,將緊隨其后的結果返回賦值給ret,通常這里的ret,就是我們文中說道的next,也就是當前中間件的下一個中間件。
拿到下一個中間件后把他交給next去處理
function next(ret) { if (ret.done) return resolve(ret.value); var value = toPromise.call(ctx, ret.value); if (value && isPromise(value)) return value.then(onFulfilled, onRejected); return onRejected(new TypeError("You may only yield a function, promise, generator, array, or object, " + "but the following object was passed: "" + String(ret.value) + """)); }
當中間件執行結束了,就把Promise的狀態設置為成功。否則就將ret(也就是下一個中間件)再用co包一次。主要看toPromise的這幾行代碼即可
function toPromise(obj) { // xxx if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); // xxx }
注意噢toPromise這個時候的返回值是一個Promise,這個非常關鍵,是下一個中間件執行完成之后回溯到上一個中間件中斷執行處繼續執行的關鍵
function next(ret) { // xxx var value = toPromise.call(ctx, ret.value); // 即通過前面toPromise返回的Promise實現,當后一個中間件執行結束,回退到上一個中間件中斷處繼續執行 if (value && isPromise(value)) return value.then(onFulfilled, onRejected); // xxx }
看到這里,我們可以總結出,幾乎koa的中間件都會被co給包裝一次,而每一個中間件又可以通過Promise的then去監測其后一個中間件是否結束,后一個中間件結束后會執行前一個中間件用then監聽的操作,這個操作便是執行該中間件yield next后面的那些代碼
打個比方:
當koa中接收到一個請求的時候,請求將經過兩個中間件,分別是中間件1和中間件2,
中間件1
// 中間件1在yield 中間件2之前的代碼 yield 中間件2 // 中間件2執行完成之后繼續執行中間件1的代碼
中間件2
// 中間件2在yield noop中間件之前的代碼 yield noop中間件 // noop中間件執行完成之后繼續執行中間件2的代碼
那么處理的過程就是co會立即調用onFulfilled來執行中間件1前半部分代碼,遇到yield 中間件2的時候得到中間件2generator對象,緊接著,又把這個對象放到co里面繼續執行一遍,以此類推下去知道最后一個中間件(我們這里的指的是那個空的noop中間件)執行結束,繼而馬上調用promise的resolve方法表示結束,ok,這個時候中間件2監聽到noop執行結束了,馬上又去執行了onFulfilled來執行yield noop中間件后半部分代碼,好啦這個時候中間件2也執行結束了,也會馬上調用promise的resolve方法表示結束,ok,這個時候中間件1監聽到中間件2執行結束了,馬上又去執行了onFulfilled來執行yield 中間件2后半部分代碼,最后中間件全部執行完了,就執行respond.call(ctx);
啊 啊 啊好繞,不過慢慢看,仔細想,還是可以想明白的。用代碼表示這個過程有點類似
new Promise((resolve, reject) => { // 我是中間件1 yield new Promise((resolve, reject) => { // 我是中間件2 yield new Promise((resolve, reject) => { // 我是body }) // 我是中間件2 }) // 我是中間件1 });結尾
羅里吧嗦說了一大堆,也不知道有沒有把執行原理說明白。
如果對你理解koa有些許幫助,不介意的話,點擊源碼地址點顆小星星吧
如果對你理解koa有些許幫助,不介意的話,點擊源碼地址點顆小星星吧
如果對你理解koa有些許幫助,不介意的話,點擊源碼地址點顆小星星吧
源碼地址
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/83062.html
答案自己谷歌或百度找。 一、來源背景 面試題是來自微博@牛客網發布的真實大廠前端面經題目,我一直在收集題目長期一個一個的記錄下來的,可能會有重復,但基本前端的面試大綱和需要掌握的知識都在其中了,面試題僅做學習參考,學習者閱后也要用心鉆研其中的原理,重要知識需要系統學習、透徹學習,形成自己的知識鏈。 二、532道前端真實大廠面試題 express和koa的對比,兩者中間件的原理,koa捕獲異常多種情...
答案自己谷歌或百度找。 一、來源背景 面試題是來自微博@牛客網發布的真實大廠前端面經題目,我一直在收集題目長期一個一個的記錄下來的,可能會有重復,但基本前端的面試大綱和需要掌握的知識都在其中了,面試題僅做學習參考,學習者閱后也要用心鉆研其中的原理,重要知識需要系統學習、透徹學習,形成自己的知識鏈。 二、532道前端真實大廠面試題 express和koa的對比,兩者中間件的原理,koa捕獲異常多種情...
答案自己谷歌或百度找。 一、來源背景 面試題是來自微博@牛客網發布的真實大廠前端面經題目,我一直在收集題目長期一個一個的記錄下來的,可能會有重復,但基本前端的面試大綱和需要掌握的知識都在其中了,面試題僅做學習參考,學習者閱后也要用心鉆研其中的原理,重要知識需要系統學習、透徹學習,形成自己的知識鏈。 二、532道前端真實大廠面試題 express和koa的對比,兩者中間件的原理,koa捕獲異常多種情...
閱讀 3708·2021-10-18 13:34
閱讀 2397·2021-08-11 11:15
閱讀 1201·2019-08-30 15:44
閱讀 687·2019-08-26 10:32
閱讀 986·2019-08-26 10:13
閱讀 2065·2019-08-23 18:36
閱讀 1775·2019-08-23 18:35
閱讀 523·2019-08-23 17:10