如何閱讀源碼--Koa爲例

最近一年零零散散看了很多開源項目的源碼, 多少也有點心得, 這裏想經過這篇文章總結一下, 這裏以Koa爲例, 前段時間其實看過Koa的源碼, 可是發現理解的有點誤差, 因此從新過一遍.git

不得不說閱讀tj的代碼真的收穫很大, 沒啥奇技淫巧, 代碼優雅, 設計極好. 註釋什麼的就更不用說了. 總之仍是推薦把他的項目都過一遍(逃)github

跑通例子

Koa做爲一個web框架, 咱們要去閱讀它的源碼確定是得知道它的用法, Koa的文檔也很簡單, 它一開始就提供了一個例子:web

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

這是啓動最基本的的web服務, 這個跑起來沒啥問題. express

一樣, 文檔也提供了做爲Koa的核心賣點的中間件的基本用法:api

const Koa = require('koa');
const app = new Koa();

// x-response-time

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// logger

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});

// response

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

上面代碼可能跟咱們以前寫的js代碼常識不太符合了, 由於async/await會暫停做案現場, 相似同步. 也就是碰到await next, 代碼會跳出當前中間件, 執行下一個, 最終還回原路返回, 依次執行await next下面的代碼, 固然這只是一個表述而已, 實際就是一個遞歸返回Promise, 後面會提到.數組

閱讀目標

好了. 咱們知道Koa怎麼用了, 那對於這個框架咱們想知道什麼呢. 先看一下源碼的目錄結構好了:promise

image

注意這個compose.js是我爲了方便修改源碼拉過來的, 其實它是額外的一個包.app

application.js 做爲入口文件確定是個構造函數
context.js 就是ctx
request.js
response.js框架

那咱們讀源碼總須要一個目標吧, 這篇文章裏咱們假定目標就是弄懂Koa的中間件原理好了koa

分析執行流程

好, 目標也有了, 下面正式進入源碼閱讀狀態. 咱們以最簡單的示例代碼做爲入口來切入Koa的執行過程:

const app = new Koa();

上面咱們能夠看到Koa是做爲構造函數引用的, 那麼咱們來看看入口文件Application.js 導出了個啥:

module.exports = class Application extends Emitter { 
 // ...
}

毫無疑問是能夠對應上的, 導出了一個類.

app.use(async ctx => {
  ctx.body = 'Hello World';
});

看上面的東西彷佛進入正題了, 咱們知道use就是引用了一箇中間件, 那來看看use是個啥玩意:

use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

太長太臭, 精簡一下

use(fn) {
    this.middleware.push(fn);
    return this;
  }

emm 這下就很清楚了, 就是維護了一箇中間件數組middleware, 到這裏不要忘了咱們的目標: Koa的中間件原理, 既然找到這個中間件數組了, 咱們就來看看它是怎麼被調用的吧. 全局搜一下, 咱們發現其實就一個方法裏用到了middleware:

callback() {
    const fn = compose(this.middleware);

    if (!this.listeners('error').length) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

上面的代碼能夠看到, 彷佛有一個compose對middleware進行處理了, 咱們好像離真相愈來愈近了

function compose (middleware) {

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

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

刪除邊界條件, 錯誤處理

compose.js的代碼很短, 可是仍是嫌長怎麼辦, 以前有文章提到的, 刪除邊界條件和異常處理:

function compose (middleware) {

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

  return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      index = i
      let fn = middleware[i]
      if (!fn) return Promise.resolve()
      return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
    }
  }
}

這麼一看就清晰多了, 不就是一個遞歸遍歷middleware嘛. 彷佛跟express有點像.

猜測結論

大膽假設嘛, 前面提到了, await 會暫停執行, 那await next 彷佛暫停的就是這裏, 而後不斷遞歸調用中間件, 而後遞歸中斷了, 代碼又從一個個的promise裏退出來, 彷佛這樣就很洋蔥了.

emm 究竟是不是這樣呢, 我也不知道. 比較還想再水一篇文章呢.

image

相關文章
相關標籤/搜索