你知道 koa 中介軟體執行原理嗎?

謙龍發表於2019-03-04

前言

原文地址

最近幾天花了比較長的時間在koa(1)的原始碼分析上面,初次看的時候,被中介軟體執行那段整的暈乎乎的,完全不知道所以,再次看,好像明白了些什麼,再反覆看,我去,簡直神了,簡直淚流滿面,簡直喪心病狂啊!!!

你知道 koa 中介軟體執行原理嗎?
koa

用在前面

下面的例子會在控制檯中列印出一些資訊(具體列印出什麼?可以猜猜?),然後返回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
  }
};複製程式碼

這段程式碼可以分成兩個部分

  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 中介軟體執行原理嗎?

我知道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有些許幫助,不介意的話,點選原始碼地址點顆小星星吧

如果對你理解koa有些許幫助,不介意的話,點選原始碼地址點顆小星星吧

原始碼地址

相關文章