koa2第二篇: 圖解中間件源碼執行過程

中間件

洋蔥模型

首先寫一個簡單的中間件demo:html

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}`)
})

複製代碼

很明顯中間件執行順序是這樣的:api

開始執行中間件1
開始執行中間件2
結束執行中間件2
結束執行中間件1
複製代碼

你能夠理解爲koa2會先按照中間件註冊順序執行next()以前的代碼, 執行完到底部以後, 返回往前執行next()以後的代碼。bash

重點是咱們須要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已經存了兩個中間件。app

  • 這個時候你請求一個路由好比
http://localhost:3000/a
複製代碼

koa2的中間件處理就是在這個函數裏面koa

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是一直存在的。async

這個時候能夠看到fn就是ctx1。函數

注意ui

// 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,從而實現文章開頭那張圖的執行順序。

相關文章
相關標籤/搜索