你知道 koa 中間件執行原理嗎?

前言

原文地址javascript

最近幾天花了比較長的時間在koa(1)的源碼分析上面,初次看的時候,被中間件執行那段整的暈乎乎的,徹底不知道因此,再次看,好像明白了些什麼,再反覆看,我去,簡直神了,簡直淚流滿面,簡直喪心病狂啊!!!java

koa

用在前面

下面的例子會在控制檯中打印出一些信息(具體打印出什麼?能夠猜猜😀),而後返回hello worldgit

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幹了嘛github

這是koa的構造函數,爲了沒有其餘信息的干擾,我去除了一些暫時用不到的代碼,這裏咱們把目光聚焦在middleware這個數組便可。api

function Application() {
  // xxx
  this.middleware = []; // 這個數組就是用來裝一個個中間件的
  // xxx
}複製代碼

接下來咱們要看use方法了數組

一樣去除了一些暫時不用的代碼,能夠看到每次執行use方法,就把外面傳進來的generator函數push到middleware數組中promise

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

好啦!你已經知道koa中是預先經過use方法,將請求可能會通過的中間件裝在了一個數組中。app

接下來咱們要開始本文的重點了,當一個請求到來的時候,是怎樣通過中間件,怎麼跑起來的koa

首先咱們只要知道下面這段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
  }
};複製代碼

這段代碼能夠分紅兩個部分

  1. 請求前的中間件初始化處理部分
  2. 請求到來時的中間件運行部分

咱們分部分來講一下

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有些許幫助,不介意的話,點擊源碼地址點顆小星星吧

源碼地址

相關文章
相關標籤/搜索