深刻探析koa之中間件流程控制篇

koa被認爲是第二代web後端開發框架,相比於前代express而言,其最大的特點無疑就是解決了回調金字塔的問題,讓異步的寫法更加的簡潔。在使用koa的過程當中,其實一直比較好奇koa內部的實現機理。最近終於有空,比較深刻的研究了一下koa一些原理,在這裏會寫一系列文章來記錄一下個人學習心得和理解。javascript

在我看來,koa最核心的函數是大名鼎鼎的co,koa正是基於這個函數實現了異步回調同步化,以及中間件流程控制。固然在這篇文章中我並不會去分析co源碼,我打算在整個系列文章中,一步一步講解如何實現koa中間件的流程控制原理,koa的異步回調同步寫法實現原理,最後在理解這些的基礎上,實現一個簡單的相似co的函數。java

本篇首先只談一談koa的中間件流程控制原理。git

1. koa中間件執行流程

關於koa中間件如何執行,官網上有一個很是經典的例子,有興趣的能夠去看看,不過這裏,我想把它修改的更簡單一點:es6

var koa = require('koa');
var app = koa();

app.use(function*(next) {
  console.log('begin middleware 1');
  yield next;
  console.log('end middleware 1');
});

app.use(function*(next) {
  console.log('begin middleware 2');
  yield next;
  console.log('end middleware 2');
});

app.use(function*() {
  console.log('middleware 3');
});

app.listen(3000);

運行這個例子,而後使用curl工具,運行:github

curl http://localhost:3000

能夠看到,運行以後,會輸出:web

begin middleware 1
begin middleware 2
middleware 3
end middleware 2
end middleware 1

這個例子很是形象的表明了koa的中間件執行機制,能夠用下圖的洋蔥模型來形容:express

圖片描述

經過這種執行流程,開發者能夠很是方便的開發一些中間件,而且很是容易的整合到實際業務流程中。那麼,這樣的流程又是如何實現和控制的呢?後端

2. koa中的generator和compose

簡單來講,洋蔥模型的執行流程是經過es6中的generator來實現的。不熟悉generator的同窗能夠去看看其特性,其中一個就是generator函數能夠像打斷點同樣從函數某個地方跳出,以後還能夠再回來繼續執行。下面一個例子能夠說明這種特性:app

var gen=function*(){
  console.log('begin!');
  //yield語句,在這裏跳出,將控制權交給anotherfunc函數。
  yield anotherfunc;
  //下次回來時候從這裏開始執行
  console.log('end!');
}

var anotherfunc(){
  console.log('this is another function!');
}

var g=gen();
var another=g.next();  //'begin!'
//another是一個對象,其中value成員就是返回的anotherfunc函數
another.value();  //'this is another function!'
g.next();  //'end!';

從這個簡單例子中,能夠看出洋蔥模型最基本的一個雛形,即yield先後的語句最早和最後執行,yield中間的代碼在中心執行。框架

如今設想一下,若是yield後面跟的函數自己就又是一個generator,會怎麼樣呢?其實就是從上面例子裏面作一個引伸:

var gen1=function*(){
  console.log('begin!');
  yield g2;
  console.log('end!');
}

var gen2=function*(){
  console.log('begin 2');
  yield anotherfunc;
  console.log('end 2');
}

var anotherfunc(){
  console.log('this is another function!');
}

var g=gen();
var g2=gen2();

var another1=g.next();  //'begin!';
var another2=another1.value.next(); //'begin 2';
another2.value(); //'this is another function!';
another1.value.next(); //'end 2';
g.next(); //'end!';

能夠看出,基本上是用上面的例子,再加一個嵌套而已,原理是同樣的。

而在koa中,每一箇中間件generator都有一個next參數。在咱們這個例子中,g2就能夠當作是g函數的next參數。事實上,koa也確實是這樣作的,當使用app.use()掛載了全部中間件以後,koa有一個koa-compose模塊,用於將全部generator中間件串聯起來,基本上就是將後一個generator賦給前一個generator的next參數。koa-compose的源碼很是簡單短小,下面是我本身實現的一個:

function compose(middlewares) {
  return function(next) {
    var i = middlewares.length;
    var next = function*() {}();
    while (i--) {
      next = middlewares[i].call(this, next);
    }
    return next;
  }
}

使用咱們本身寫的compose對上面一個例子改造,是的其更接近koa的形式:

function compose(middlewares) {
  return function(next) {
    var i = middlewares.length;
    var next = function*() {}();
    while (i--) {
      next = middlewares[i].call(this, next);
    }
    return next;
  }
}

var gen1=function*(next){
  console.log('begin!');
  yield next;
  console.log('end!');
}

var gen2=function*(next){
  console.log('begin 2');
  yield next;
  console.log('end 2');
}

var gen3=function*(next){
  console.log('this is another function!');
}

var bundle=compose([gen1,gen2,gen3]);
var g=bundle();

var another1=g.next();  //'begin!';
var another2=another1.value.next(); //'begin 2';
another2.value.next(); //'this is another function!';
another1.value.next(); //'end 2';
g.next(); //'end!';

怎麼樣?是否是有一點koa中間件寫法的感受了呢?可是目前,咱們仍是一步一步手動的在執行咱們這個洋蔥模型,可否寫一個函數,自動的來執行咱們這個模型呢?

3. 讓洋蔥模型自動跑起來:一個run函數的編寫

上面例子中,最後的代碼咱們能夠看出一個規律,基本就是外層的generator調用next方法把控制權交給內層,內層再繼續調用next把方法交給更裏面的一層。整個流程能夠用一個函數嵌套的寫法寫出來。話很少說,直接上代碼:

function run(gen) {
  var g;
  if (typeof gen.next === 'function') {
    g = gen;
  } else {
    g = gen();
  }
  function next() {
    var tmp = g.next();
    //若是tmp.done爲true,那麼證實generator執行結束,返回。
    if (tmp.done) {
      return;
    } else if (typeof g.next === 'function') {
      run(tmp.value);
      next();
    }
  }
  next();
}

function compose(middlewares) {
  return function(next) {
    var i = middlewares.length;
    var next = function*() {}();
    while (i--) {
      next = middlewares[i].call(this, next);
    }
    return next;
  }
}

var gen1 = function*(next) {
  console.log('begin!');
  yield next;
  console.log('end!');
}

var gen2 = function*(next) {
  console.log('begin 2');
  yield next;
  console.log('end 2');
}

var gen3 = function*(next) {
  console.log('this is another function!');
}

var bundle = compose([gen1, gen2, gen3]);

run(bundle);

run函數接受一個generator,其內部執行其實就是咱們上一個例子的精簡,使用遞歸的方法執行。運行這個例子,能夠看到結果和咱們上一個例子相同。

到此爲止,咱們就基本講清楚了koa中的中間件洋蔥模型是如何自動執行的。事實上,koa中使用的co函數,一部分功能就是實現咱們這裏編寫的run函數的功能。

值得注意的是,這篇文章只注重分析中間件執行流程的實現,暫時並無考慮異步回調同步化原理。下一篇文章中,我將帶你們繼續探析koa中異步回調同步化寫法的機理。

這篇文章的代碼能夠在github上面找到:https://github.com/mly-zju/async-js-demo,其中process_control.js文件就是本篇的事例源碼。

另外歡迎多多關注個人我的博客哦^_^ 會不按期更新個人技術文章~

相關文章
相關標籤/搜索