你知道koa中间件执行原理吗?

前言

原文地址

最近几天花了比较长的时间在koa(1)的源码分析上面,初次看的时候,�被中间件执行那段整的晕乎乎的,完全不知道所以,再次看,好像明白了些什么,再反复看,我去,�简直神了,简直泪流满面,简直丧心病狂啊!!!

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很屌,但也别这么屌好不好,所以说评价一个好的程序员不是由代码量决定的

我们来看下这段代码到底有什么神奇的地方

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有些许帮助,不介意的话,点击源码地址点颗小星星吧

源码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,597评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,053评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,583评论 0 326
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,888评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,772评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,536评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,922评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,554评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,838评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,861评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,677评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,483评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,928评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,104评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,403评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,945评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,156评论 2 339

推荐阅读更多精彩内容