你知道koa中間件執(zhí)行原理嗎?

前言

原文地址

最近幾天花了比較長(zhǎng)的時(shí)間在koa(1)的源碼分析上面顿颅,初次看的時(shí)候缸濒,?被中間件執(zhí)行那段整的暈乎乎的,完全不知道所以粱腻,再次看庇配,好像明白了些什么,再反復(fù)看绍些,我去捞慌,?簡(jiǎn)直神了,簡(jiǎn)直淚流滿面柬批,簡(jiǎn)直喪心病狂靶ピ琛!B芸臁锻霎!

koa

用在前面

下面的例子會(huì)在控制臺(tái)中打印出一些信息(具體打印出什么?可以猜猜??)揪漩,然后返回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)

用過(guò)koa的同學(xué)都知道添加中間件的方式是使用koa實(shí)例的use方法,并傳入一個(gè)generator函數(shù)奄容,這個(gè)generator函數(shù)可以接受一個(gè)next(這個(gè)next到底是啥冰更?這里先不闡明,在后面會(huì)仔細(xì)說(shuō)明)昂勒。

執(zhí)行use干了嘛

這是koa的構(gòu)造函數(shù)蜀细,為了沒(méi)有其他信息的干擾,我去除了一些暫時(shí)用不到的代碼戈盈,這里我們把目光聚焦?在middleware這個(gè)數(shù)組即可奠衔。

function Application() {
  // xxx
  this.middleware = []; // 這個(gè)數(shù)組就是用來(lái)裝一個(gè)個(gè)中間件的
  // xxx
}

?接下來(lái)我們要看use方法了

同樣去除了一些暫時(shí)不用的代碼,可以看到每次執(zhí)行use方法塘娶,就把外面?zhèn)鬟M(jìn)來(lái)的generator函數(shù)push到middleware數(shù)組中


app.use = function(fn){
  // xxx
  this.middleware.push(fn);
  // xxx
};


好啦归斤!你已經(jīng)知道koa中是預(yù)先通過(guò)use方法,將請(qǐng)求可能會(huì)經(jīng)過(guò)的中間件裝在了一個(gè)數(shù)組中刁岸。

接下來(lái)我們要開(kāi)始本文的重點(diǎn)了脏里,當(dāng)一個(gè)請(qǐng)求?到來(lái)的時(shí)候,是怎樣經(jīng)過(guò)中間件虹曙,怎么跑起來(lái)的

首先我們只要知道下面這段callback函數(shù)就是請(qǐng)求到來(lái)的時(shí)候執(zhí)行的回調(diào)即可(同樣盡量去除了我們不用的代碼)


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
  }
};


這段代碼可以分成兩個(gè)部分

  1. 請(qǐng)求前的中間件初始化處理部分
  2. 請(qǐng)求到來(lái)時(shí)的中間件運(yùn)行部分

我們分部分來(lái)說(shuō)一下


var fn = this.experimental
    ? compose_es7(this.middleware)
    : co.wrap(compose(this.middleware));

這段代碼對(duì)experimental做了下判斷迫横,如果設(shè)置為了true那么koa中將可以支持傳入async函數(shù)番舆,否則就執(zhí)行co.wrap(compose(this.middleware))。?

只有一行初始化中間件就做完啦矾踱?

我知道koa很屌恨狈,但也別這么屌好不好,所以說(shuō)評(píng)價(jià)一個(gè)好的程序員不是由代碼量決定的

我們來(lái)看下這段代碼到底有什么神奇的地方

compose(this.middleware)

把裝著中間件middleware的數(shù)組作為參數(shù)傳進(jìn)了compose這個(gè)方法介返,那么compose做了什么事呢拴事?其實(shí)就是把原本毫無(wú)關(guān)系的一個(gè)個(gè)中間件給首尾串起來(lái)了沃斤,于是他們之間?就有了千絲萬(wàn)縷的聯(lián)系圣蝎。

function compose(middleware){
  return function *(next){
    // 第一次得到next是由于*noop生成的generator對(duì)象
    if (!next) next = noop(); 

    var i = middleware.length;
    // 從后往前開(kāi)始執(zhí)行middleware中的generator函數(shù)
    while (i--) {
      // 把后一個(gè)?中間件得到的generator?對(duì)象傳給前一個(gè)作為第一個(gè)參數(shù)存在
      next = middleware[i].call(this, next);
    }
    
    return yield *next;
  }
}

function *noop(){}

文字解釋一下就是,compose將中間件從最后一個(gè)開(kāi)始處理衡瓶,并一直往前?直到第一個(gè)中間件徘公。其中非常關(guān)鍵的就是將后一個(gè)中間件得到generator對(duì)象作為參數(shù)(這個(gè)參數(shù)就是文章開(kāi)頭說(shuō)到的next啦,也就是說(shuō)next其實(shí)是一個(gè)?generator對(duì)象)傳給前一個(gè)中間件哮针。當(dāng)然最后一個(gè)中間件的參數(shù)next是一個(gè)空的generator函數(shù)?生成的對(duì)象关面。

我們自己來(lái)寫(xiě)一個(gè)?簡(jiǎn)單的例子說(shuō)明compose是如何將多個(gè)generator函數(shù)串聯(lián)起來(lái)的

function * gen1 (next) {
  yield 'gen1'
  yield * next // 開(kāi)始執(zhí)行下一個(gè)中間件
  yield 'gen1-end' // 下一個(gè)中間件執(zhí)行完成再繼續(xù)執(zhí)行g(shù)en1中間件的邏輯
}

function * gen2 (next) {
  yield 'gen2'
  yield * next // 開(kāi)始執(zhí)行下一個(gè)中間件
  yield 'gen2-end' // 下一個(gè)中間件執(zhí)行完成再繼續(xù)執(zhí)行g(shù)en2中間件的邏輯
}

function * gen3 (next) {
  yield 'gen3'
  yield * next // 開(kāi)始執(zhí)行下一個(gè)中間件
  yield 'gen3-end' // 下一個(gè)中間件執(zhí)行完成再繼續(xù)執(zhí)行g(shù)en3中間件的邏輯
}

function * noop () {}

var middleware = [gen1, gen2, gen3]
var len = middleware.length
var next = noop() // 提供給最后一個(gè)中間件的參數(shù)

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}

看到了嗎?中間件?被串起來(lái)之后執(zhí)行的順序是

gen1 -> gen2 -> gen3 -> noop -> gen3 -> gen2 -> gen1

從而首尾相連十厢,進(jìn)而發(fā)生了關(guān)系??等太。

co.wrap

通過(guò)compose處理后返回了一個(gè)generator函數(shù)。

co.wrap(compose(this.middleware))

所有上述代碼可以理解為

co.wrap(function * gen ())

好蛮放,我們?cè)倏纯?code>co.wrap做了什么,慢慢地一步步靠近了哦

co.wrap = function (fn) {
  createPromise.__generatorFunction__ = fn;
  return createPromise;
  function createPromise() {
    return co.call(this, fn.apply(this, arguments));
  }
}

可以看到?co.wrap返回了一個(gè)普通函數(shù)createPromise,這個(gè)函數(shù)就是文章開(kāi)頭的fn啦缩抡。

var fn = this.experimental
    ? compose_es7(this.middleware)
    : co.wrap(compose(this.middleware));

中間件開(kāi)始跑起來(lái)啦

前面已經(jīng)說(shuō)完了,中間件是如何初始化的包颁,即如果由不相干到關(guān)系密切了瞻想,接下來(lái)開(kāi)始說(shuō)請(qǐng)求到來(lái)時(shí),初始化好的中間件?是怎么跑的娩嚼。

fn.call(ctx).then(function () {
  respond.call(ctx);
}).catch(ctx.onerror);

這一段便是請(qǐng)求到來(lái)手即將要經(jīng)過(guò)的中間件執(zhí)行部分蘑险,fn執(zhí)行之后返回的是一個(gè)Promise,koa通過(guò)注冊(cè)成功和失敗的回調(diào)函數(shù)來(lái)分別處理請(qǐng)求岳悟。

讓我們回到

co.wrap = function (fn) {
  // xxx
  
  function createPromise() {
    return co.call(this, fn.apply(this, arguments));
  }
}

createPromise里面的fn就是經(jīng)過(guò)compose處理中間件后返回的一個(gè)generator函數(shù)佃迄,那么執(zhí)行之后拿到的就是一個(gè)generator對(duì)象了,并把這個(gè)對(duì)象傳經(jīng)經(jīng)典的co里面啦贵少。如果你需要對(duì)co的源碼了解歡迎查看昨天寫(xiě)的走一步再走一步呵俏,揭開(kāi)co的神秘面紗,好了春瞬,接下來(lái)就是看co里面如何處理這個(gè)被compose處理過(guò)的generator對(duì)象了

再回顧一下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,這個(gè)時(shí)候第一次進(jìn)co的時(shí)候因?yàn)橐呀?jīng)是generator對(duì)象所以會(huì)直接執(zhí)行onFulfilled()

function onFulfilled(res) {
  var ret;
  try {
    ret = gen.next(res);
  } catch (e) {
    return reject(e);
  }
  next(ret);
}

gen.next正是用于去執(zhí)行中間件的業(yè)務(wù)邏輯柴信,當(dāng)遇到y(tǒng)ield語(yǔ)句的時(shí)候,將?緊隨其后的結(jié)果返回賦值給ret,通常這里的ret宽气,就是我們文中說(shuō)道的next,也就是當(dāng)前中間件的下一個(gè)中間件随常。

拿到下一個(gè)中間件后把他交給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) + '"'));
}

當(dāng)中間件執(zhí)行結(jié)束了潜沦,就把Promise的狀態(tài)設(shè)置為成功。否則就將ret(也就是下一個(gè)中間件)再用co包一次绪氛。主要看toPromise的這幾行代碼即可


function toPromise(obj) {
  // xxx
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  // xxx
}


注意噢toPromise這個(gè)時(shí)候的返回值是一個(gè)Promise唆鸡,這個(gè)非常關(guān)鍵,是下一個(gè)中間件執(zhí)行完成之后回溯到上一個(gè)中間件中斷執(zhí)行處繼續(xù)執(zhí)行的關(guān)鍵

function next(ret) {
  // xxx
  var value = toPromise.call(ctx, ret.value);
  // 即通過(guò)前面toPromise返回的Promise實(shí)現(xiàn)枣察,當(dāng)后一個(gè)中間件執(zhí)行結(jié)束争占,回退到上一個(gè)中間件中斷處繼續(xù)執(zhí)行
  if (value && isPromise(value)) return value.then(onFulfilled, onRejected); 
  // xxx 
}

看到這里,我們可以總結(jié)出序目,幾乎koa的中間件都會(huì)被co給包裝一次臂痕,而?每一個(gè)中間件又可以通過(guò)Promise的then去監(jiān)測(cè)其后一個(gè)?中間件是否結(jié)束,后一個(gè)中間件結(jié)束后?會(huì)執(zhí)行前一個(gè)中間件用then監(jiān)聽(tīng)的操作猿涨,這個(gè)操作便是執(zhí)行該中間件yield next后面的那些代碼

打個(gè)比方:

當(dāng)koa中接收到一個(gè)請(qǐng)求的時(shí)候握童,?請(qǐng)求將經(jīng)過(guò)兩個(gè)中間件,分別是中間件1中間件2叛赚,

中間件1

// 中間件1在yield 中間件2之前的代碼

yield 中間件2

// 中間件2執(zhí)行完成之后繼續(xù)執(zhí)行中間件1的代碼

中間件2

// 中間件2在yield noop中間件之前的代碼

yield noop中間件

// noop中間件執(zhí)行完成之后繼續(xù)執(zhí)行中間件2的代碼

那么處理的過(guò)程就是co會(huì)立即調(diào)用onFulfilled來(lái)執(zhí)行中間件1前半部分代碼澡绩,遇到yield 中間件2的時(shí)候得到中間件2generator對(duì)象,緊接著俺附,又把這個(gè)對(duì)象放到co里面繼續(xù)執(zhí)行一遍肥卡,以此類推下去知道最后一個(gè)中間件(我們這里的?指的是那個(gè)空的noop中間件)執(zhí)行結(jié)束,繼而馬上調(diào)用promise的resolve方法表示結(jié)束事镣,ok步鉴,這個(gè)時(shí)候中間件2監(jiān)聽(tīng)到noop執(zhí)行結(jié)束了,馬上又去執(zhí)行了onFulfilled來(lái)執(zhí)行yield noop中間件后半部分代碼蛮浑,好啦這個(gè)?時(shí)候中間件2也執(zhí)行結(jié)束了唠叛,也會(huì)馬上調(diào)用promise的resolve方法表示結(jié)束,??ok,這個(gè)時(shí)候中間件1監(jiān)聽(tīng)到中間件2執(zhí)行結(jié)束了沮稚,馬上又去執(zhí)行了onFulfilled來(lái)執(zhí)行yield 中間件2后半部分代碼艺沼,最后中間件全部執(zhí)行完了,就執(zhí)行respond.call(ctx);

啊 啊 啊好繞蕴掏,不過(guò)慢慢看障般,仔細(xì)想,還是可以想明白的盛杰。用代碼表示這個(gè)過(guò)程有點(diǎn)類似

new Promise((resolve, reject) => {
  // 我是中間件1
  yield new Promise((resolve, reject) => {
    // 我是中間件2
    yield new Promise((resolve, reject) => {
      // 我是body
    })
    // 我是中間件2
  })
  // 我是中間件1
});


中間件執(zhí)行順序

結(jié)尾

羅里吧嗦說(shuō)了一大堆挽荡,也不知道有沒(méi)有把執(zhí)行原理說(shuō)明白。

如果對(duì)你理解koa有些許幫助即供,不介意的話定拟,點(diǎn)擊源碼地址點(diǎn)顆小星星吧

如果對(duì)你理解koa有些許幫助,不介意的話逗嫡,點(diǎn)擊源碼地址點(diǎn)顆小星星吧

如果對(duì)你理解koa有些許幫助青自,不介意的話株依,點(diǎn)擊源碼地址點(diǎn)顆小星星吧

源碼地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市延窜,隨后出現(xiàn)的幾起案子恋腕,更是在濱河造成了極大的恐慌,老刑警劉巖逆瑞,帶你破解...
    沈念sama閱讀 222,464評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荠藤,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡获高,警方通過(guò)查閱死者的電腦和手機(jī)哈肖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)谋减,“玉大人牡彻,你說(shuō)我怎么就攤上這事扫沼〕龅” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,078評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵缎除,是天一觀的道長(zhǎng)严就。 經(jīng)常有香客問(wèn)我,道長(zhǎng)器罐,這世上最難降的妖魔是什么梢为? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,979評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮轰坊,結(jié)果婚禮上铸董,老公的妹妹穿的比我還像新娘。我一直安慰自己肴沫,他們只是感情好粟害,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著颤芬,像睡著了一般悲幅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上站蝠,一...
    開(kāi)封第一講書(shū)人閱讀 52,584評(píng)論 1 312
  • 那天汰具,我揣著相機(jī)與錄音,去河邊找鬼菱魔。 笑死留荔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的澜倦。 我是一名探鬼主播聚蝶,決...
    沈念sama閱讀 41,085評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼拔疚,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了既荚?” 一聲冷哼從身側(cè)響起稚失,我...
    開(kāi)封第一講書(shū)人閱讀 40,023評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎恰聘,沒(méi)想到半個(gè)月后句各,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,555評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晴叨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評(píng)論 3 342
  • 正文 我和宋清朗相戀三年凿宾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兼蕊。...
    茶點(diǎn)故事閱讀 40,769評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡初厚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出孙技,到底是詐尸還是另有隱情产禾,我是刑警寧澤,帶...
    沈念sama閱讀 36,439評(píng)論 5 351
  • 正文 年R本政府宣布牵啦,位于F島的核電站亚情,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏哈雏。R本人自食惡果不足惜楞件,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望裳瘪。 院中可真熱鬧土浸,春花似錦、人聲如沸彭羹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,601評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)皆怕。三九已至毅舆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間愈腾,已是汗流浹背憋活。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,702評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留虱黄,地道東北人悦即。 一個(gè)月前我還...
    沈念sama閱讀 49,191評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親辜梳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子粱甫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 陸陸續(xù)續(xù)用了koa和co也算差不多用了大半年了,大部分的場(chǎng)景都是在服務(wù)端使用koa來(lái)作為restful服務(wù)器用作瞄,使...
    Sunil閱讀 1,551評(píng)論 0 3
  • 說(shuō)起Node茶宵,最常用的估計(jì)就是express和koa,兩者都用到了中間件(middleware)這一概念宗挥,主要用于...
    慕容跳凱閱讀 4,628評(píng)論 0 4
  • 看到標(biāo)題乌庶,也許您會(huì)覺(jué)得奇怪,redux跟Koa以及Express并不是同一類別的框架契耿,干嘛要拿來(lái)做類比瞒大。盡管,例如...
    Perkin_閱讀 1,731評(píng)論 0 4
  • 弄懂js異步 講異步之前搪桂,我們必須掌握一個(gè)基礎(chǔ)知識(shí)-event-loop透敌。 我們知道JavaScript的一大特點(diǎn)...
    DCbryant閱讀 2,714評(píng)論 0 5
  • 明天不一定美好裸燎,你努力顾瞻,就一切美好。 黎明的到來(lái)德绿,是告訴你明天到來(lái)的鬧鐘,是明天告訴你到來(lái)的禮儀退渗。也許移稳,沒(méi)有那么期...
    Kayan阿嘉閱讀 821評(píng)論 0 19