阅读 668

koa2第二篇: 图解中间件源码执行过程

中间件

洋葱模型

首先写一个简单的中间件demo:

const Koa = require('koa')
const app = new Koa()
const port = 3000

const ctx1 = async (ctx, next) => {
    console.log('开始执行中间件1')
    await next()
    ctx.response.type = 'text/html'
    ctx.response.body = '<h3>hello world</h3>'
    console.log('结束执行中间件1')
}

app.use(ctx1)
app.use(async function ctx2 (ctx, next) {
    console.log('开始执行中间件2')
    await next()
    console.log('结束执行中间件2')
})

app.listen(port, () => {
    console.log(`server is running on the port: ${port}`)
})

复制代码

很明显中间件执行顺序是这样的:

开始执行中间件1
开始执行中间件2
结束执行中间件2
结束执行中间件1
复制代码

你可以理解为koa2会先按照中间件注册顺序执行next()之前的代码, 执行完到底部之后, 返回往前执行next()之后的代码。

重点是我们需要koa2源码究竟是怎么样执行的? 现在开始调试模式进入koa2源码一探究竟。

  • 首先在两个中间件注册的地方打了断点

  • 我们可以看到koa2是先按照你中间件的顺序去注册执行

  • 然后会进入callback. 这是因为
// 应用程序
app.listen(port, () => {
    console.log(`server is running on the port: ${port}`)
})

// 源码
 listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
复制代码

这个时候this.middleware已经存了两个中间件。

  • 这个时候你请求一个路由比如
http://localhost:3000/a
复制代码

koa2的中间件处理就是在这个函数里面

callback() {
    // compose()这是处理中间件的执行顺序所在
  }
复制代码

于是我们进入这个koa-compose的源码看下:

'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  // 首先是一些中间件格式校验
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    // 返回一个函数, 从第一个中间件开始执行, 可以通过next()调用后续中间件
    return dispatch(0)
    // dispatch始终返回一个Promise对象
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        // next即就是通过dispatch(i+1)来执行下一个中间件
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        // 捕获中间件中发生的异常
        return Promise.reject(err)
      }
    }
  }
}

复制代码

此时i=0取出第一个中间件,由于闭包原因i是一直存在的。

这个时候可以看到fn就是ctx1。

注意

// next即就是通过dispatch(i+1)来执行下一个中间件
dispatch.bind(null, i + 1)
复制代码

这个时候开始进入第一个中间件执行第一句console.log('开始执行中间件1')

这里也能看到next指的就是前面提到的dispatch.bind。

然后我们继续单步调试进入这句

// ctx1中的
await next()
复制代码

此时又重新进入compose(), 继续执行下一个中间件, i=1

取出第二个中间件函数ctx2。

此时进入第二个中间件ctx2开始执行console.log('开始执行中间件2')

继续单步调试

此时i=2,fx=undefined

// 这个洋葱模型的最后做一个兜底的处理
if (!fn) return Promise.resolve()
复制代码

执行中间件ctx2的第二句console

补充下async的执行机制: async 的执行机制是:只有当所有的 await 异步都执行完之后才能返回一个 Promise。所以当我们用 async的语法写中间件的时候,执行流程大致如下:

先执行第一个中间件(因为compose会默认执行dispatch(0)),该中间件返回 Promise,然后被Koa监听,执行对应的逻辑(成功或失败)在执行第一个中间件的逻辑时,遇到 await next()时,会继续执行dispatch(i+1),也就是执行 dispatch(1),会手动触发执行第二个中间件。

这时候,第一个中间件 await next() 后面的代码就会被 pending,等待 await next() 返回 Promise,才会继续执行第一个中间件 await next() 后面的代码。

同样的在执行第二个中间件的时候,遇到await next()的时候,会手动执行第三个中间件,await next() 后面的代码依然被 pending,等待 await 下一个中间件的Promise.resolve。

只有在接收到第三个中间件的 resolve 后才会执行后面的代码,然后第二个中间会返回 Promise,被第一个中间件的 await 捕获,这时候才会执行第一个中间件的后续代码,然后再返回 Promise 以此类推。

如果有多个中间件的时候,会依照上面的逻辑不断执行,先执行第一个中间件,在 await next() 出 pending,继续执行第二个中间件,继续在 await next() 出 pending,继续执行第三个中间,直到最后一个中间件执行完,然后返回 Promise,然后倒数第二个中间件才执行后续的代码并返回Promise,然后是倒数第三个中间件,接着一直以这种方式执行直到第一个中间件执行完,并返回 Promise,从而实现文章开头那张图的执行顺序。

关注下面的标签,发现更多相似文章
评论