koa源碼中的promise的解析

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

前端全棧學習交流圈:866109386,面向1-3經驗年前端開發人員,幫助突破技術瓶頸,提升思維能力,羣內有大量PDF可供自取,更有乾貨實戰項目視頻進羣免費領取。

上述代碼展示了 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 封裝以便在中間件執行過程中能夠將捕獲到的異常進行統一處理。 以上述編寫的應用示例代碼作爲例子畫出函數執行調用棧示意圖如下: 整個 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()
}

前端全棧學習交流圈:866109386,面向1-3經驗年前端開發人員,幫助突破技術瓶頸,提升思維能力,羣內有大量PDF可供自取,更有乾貨實戰項目視頻進羣免費領取。

從上面代碼可以看出,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) +
   '"'
  )
  );
 }
 });
}

前端全棧學習交流圈:866109386,面向1-3經驗年前端開發人員,幫助突破技術瓶頸,提升思維能力,羣內有大量PDF可供自取,更有乾貨實戰項目視頻進羣免費領取。

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

結語

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章