koa源码中的promise的解析

koa 是一个很是轻量优雅的 node 应用开发框架,趁着闲余时间阅读了下源代码,其中一些比较有意思的地方整理成文与你们分享一下。前端

洋葱型中间件机制的实现原理node

咱们常常把 koa 中间件的执行机制类比于剥洋葱,这样设计其执行顺序的好处是咱们再也不须要手动去管理 request 和 response 的业务执行流程,且一个中间件对于 request 和 response 的不一样逻辑可以放在同一个函数中,能够帮助咱们极大的简化代码。在了解其实现原理以前,先来介绍一下 koa 的总体代码结构:git

lib
|-- application.js
|-- context.js
|-- request.js
|-- response.js

application 是整个应用的入口,提供 koa constructor 以及实例方法属性的定义。context 封装了koa ctx 对象的原型对象,同时提供了对 response 和 request 对象下许多属性方法的代理访问,request.js 和 response.js 分别定义了ctx request 和 response 属性的原型对象。github

接下来让咱们来看 application.js中的一段代码:api

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 中间件应用示例代码以下所示:promise

// 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 行而已,仍是很是值得围观学习的。app

generator函数类型中间件的执行框架

v1 版本的 koa 其中间件主流支持的是 generator 函数,在 v2 以后改而支持 async/await 模式,若是依旧使用 generator,koa 会给出一个 deprecated 提示,可是为了向后兼容,目前 generator 函数类型的中间件依然可以执行,koa 内部利用 koa-convert 模块对 generator 函数进行了一层包装,请看代码:koa

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,除了应用于平常的异步流程控制,在开发过程当中咱们还能够大大挖掘其潜力,帮助咱们完成一些自动化程序工做流的事情。

相关文章
相关标签/搜索