koa原始碼中的promise

墨箏發表於2018-11-12

koa 是一個非常輕量優雅的 node 應用開發框架,趁著雙十一值班的空當閱讀了下其原始碼,其中一些比較有意思的地方整理成文與大家分享一下。

洋蔥型中介軟體機制的實現原理

我們經常把 koa 中介軟體的執行機制類比於剝洋蔥,這樣設計其執行順序的好處是我們不再需要手動去管理 request 和 response 的業務執行流程,且一箇中介軟體對於 request 和 response 的不同邏輯能夠放在同一個函式中,可以幫助我們極大的簡化程式碼。在瞭解其實現原理之前,先來介紹一下 koa 的整體程式碼結構:

lib
|-- application.js
|-- context.js
|-- request.js
|-- response.js
複製程式碼

application 是整個應用的入口,提供 koa constructor 以及例項方法屬性的定義。context 封裝了koa ctx 物件的原型物件,同時提供了對 response 和 request 物件下許多屬性方法的代理訪問,request.js 和 response.js 分別定義了ctx request 和 response 屬性的原型物件。

接下來讓我們來看 application.js中的一段程式碼:

listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
}
callback() {
    const fn = compose(this.middleware);

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

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

    return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製程式碼

上述程式碼展示了 koa 的基本原理,在其例項方法 listen 中對 http.createServer 進行了封裝 ,然後在回撥函式中執行 koa 的中介軟體,在 callback 中,this.middleware 為業務定義的中介軟體函式所構成的陣列,compose 為 koa-compose 模組提供的方法,它對中介軟體進行了整合,是構建 koa 洋蔥型中介軟體模型的奧妙所在。從 handleRequest 方法中可以看出 compose 方法執行返回的是一個函式,且該函式的執行結果是一個 promise。接下來我們就來一探究竟,看看 koa-compose 是如何做到這些的,其 原始碼和一段 koa 中介軟體應用示例程式碼如下所示:

// compose原始碼
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!')
  }
  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    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 {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

/*
** 中介軟體應用示例程式碼
*/
let Koa = require('koa')
let app = new Koa()
app.use(async function ware0 (ctx, next) {
  await setTimeout(function () {
    console.log('ware0 request')
  }, 0)
  next()
  console.log('ware0 response')
})
app.use(function ware1 (ctx, next) {
  console.log('ware1 request')
  next()
  console.log('ware1 response')
})
// 執行結果
ware0 request
ware1 request

ware1 response
ware0 response
複製程式碼

從上述 compose 的原始碼可以看出,每個中介軟體所接受的 next 函式入參都是在 compose 返回函式中定義的 dispatch 函式,dispatch接受下一個中介軟體在 middlewares 陣列中的索引作為入參,該索引就像一個遊標一樣,每當 next 函式執行後,遊標向後移一位,以獲取 middlaware 陣列中的下一個中介軟體函式 進行執行,直到陣列中最後一箇中介軟體也就是使用 app.use 方法新增的最後一箇中介軟體執行完畢之後再依次 回溯執行。整個流程實際上就是函式的呼叫棧,next 函式的執行就是下一個中介軟體的執行,只是 koa 在函式基礎上加了一層 promise 封裝以便在中介軟體執行過程中能夠將捕獲到的異常進行統一處理。 以上述編寫的應用示例程式碼作為例子畫出函式執行呼叫棧示意圖如下:

koa原始碼中的promise

整個 compose 方法的實現非常簡潔,核心程式碼僅僅 17 行而已,還是非常值得圍觀學習的。

generator函式型別中介軟體的執行

v1 版本的 koa 其中介軟體主流支援的是 generator 函式,在 v2 之後改而支援 async/await 模式,如果依舊使用 generator,koa 會給出一個 deprecated 提示,但是為了向後相容,目前 generator 函式型別的中介軟體依然能夠執行,koa 內部利用 koa-convert 模組對 generator 函式進行了一層包裝,請看程式碼:

function convert (mw) {
  // mw為generator中介軟體
  if (typeof mw !== 'function') {
    throw new TypeError('middleware must be a function')
  }
  if (mw.constructor.name !== 'GeneratorFunction') {
    // assume it's Promise-based middleware
    return mw
  }
  const converted = function (ctx, next) {
    return co.call(ctx, mw.call(ctx, createGenerator(next)))
  }
  converted._name = mw._name || mw.name
  return converted
}

function * createGenerator (next) {
  return yield next()
}
複製程式碼

從上面程式碼可以看出,koa-convert 在 generator 外部包裹了一個函式來提供與其他中介軟體一致的介面,內部利用 co 模組來執行 generator 函式,這裡我想聊的就是 co 模組的原理,generator 函式執行時並不會立即執行其內部邏輯,而是返回一個遍歷器物件,然後通過呼叫該遍歷器物件的 next 方法來執行,generator 函式本質來說是一個狀態機,如果內部有多個 yield 表示式,就需要 next 方法執行多次才能完成函式體的執行,而 co 模組的能力就是實現 generator 函式的 自動執行,不需要手動多次呼叫 next 方法,那麼它是如何做到的呢?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);
      // toPromise是一個函式,返回一個promise示例
      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) +
            '"'
        )
      );
    }
  });
}
複製程式碼

從 co 原始碼來看,它先是手動執行了一次onFulfilled 函式來觸發 generator 遍歷器物件的 next 方法,然後利用promise的onFulfilled 函式去自動完成剩餘狀態機的執行,在onRejected 中利用遍歷器物件的 throw 方法丟擲執行上一次 yield 過程中遇到的異常,整個實現過程可以說是相當簡潔優雅。

結語

通過上面的例子可以看出 promise 的能量是非常強大的,koa 的中介軟體實現和 co 模組的實現都是基於 promise,除了應用於日常的非同步流程控制,在開發過程中我們還可以大大挖掘其潛力,幫助我們完成一些自動化程式工作流的事情。

相關文章