koa源码中promise的解读


Posted in Javascript onNovember 13, 2018

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jQuery开发者都需要知道的5个小技巧
Jan 08 Javascript
详解JavaScript中void语句的使用
Jun 04 Javascript
jQuery插件datalist实现很好看的input下拉列表
Jul 14 Javascript
解决angular的$http.post()提交数据时后台接收不到参数值问题的方法
Dec 10 Javascript
基于JavaScript实现文字超出部分隐藏
Feb 29 Javascript
javascript学习指南之回调问题
Apr 23 Javascript
Vue 进阶教程之v-model详解
May 06 Javascript
Vue CLI3 如何支持less的方法示例
Aug 29 Javascript
JS实现普通轮播图特效
Jan 01 Javascript
JavaScript进制转换实现方法解析
Jan 18 Javascript
vue实现列表滚动的过渡动画
Jun 29 Javascript
Vue实现导航栏菜单
Aug 19 Javascript
vue-router传递参数的几种方式实例详解
Nov 13 #Javascript
vue-router的使用方法及含参数的配置方法
Nov 13 #Javascript
webpack 从指定入口文件中提取公共文件的方法
Nov 13 #Javascript
详解Vue实战指南之依赖注入(provide/inject)
Nov 13 #Javascript
node.js爬取中关村的在线电瓶车信息
Nov 13 #Javascript
详解Vue SSR( Vue2 + Koa2 + Webpack4)配置指南
Nov 13 #Javascript
详解Vue组件插槽的使用以及调用组件内的方法
Nov 13 #Javascript
You might like
php session处理的定制
2009/03/16 PHP
sphinx增量索引的一个问题
2011/06/14 PHP
可以保证单词完整性的PHP英文字符串截取代码分享
2014/07/15 PHP
php并发加锁示例
2016/10/17 PHP
php删除数组指定元素实现代码
2017/05/03 PHP
PHP基于cookie实现统计在线人数功能示例
2019/01/16 PHP
分享20多个很棒的jQuery 文件上传插件或教程
2011/09/04 Javascript
js如何获取file控件的完整路径具体实现代码
2013/05/15 Javascript
基于JavaScript 下namespace 功能的简单分析
2013/07/05 Javascript
jquery ajax 调用失败的原因示例介绍
2013/09/27 Javascript
Javascript浅谈之this
2013/12/17 Javascript
JS+CSS实现自动切换的网页滑动门菜单效果代码
2015/09/14 Javascript
JavaScript遍历求解数独问题的主要思路小结
2016/06/12 Javascript
javascript经典特效分享 手风琴、轮播图、图片滑动
2016/09/14 Javascript
jQuery+vue.js实现的九宫格拼图游戏完整实例【附源码下载】
2017/09/12 jQuery
jQuery+Cookie实现切换皮肤功能【附源码下载】
2018/03/25 jQuery
详解微信小程序框架wepy踩坑记录(与vue对比)
2019/03/12 Javascript
微信小程序实现搜索历史功能
2020/03/26 Javascript
js+css实现扇形导航效果
2020/08/18 Javascript
[58:46]OG vs NAVI 2019国际邀请赛小组赛 BO2 第二场 8.15
2019/08/17 DOTA
利用python将json数据转换为csv格式的方法
2018/03/22 Python
用python编写第一个IDA插件的实例
2018/05/29 Python
Python OpenCV 使用滑动条来调整函数参数的方法
2019/07/08 Python
python各类经纬度转换的实例代码
2019/08/08 Python
有趣的Python图片制作之如何用QQ好友头像拼接出里昂
2020/04/22 Python
Python实现将元组中的元素作为参数传入函数的操作
2020/06/05 Python
python实现录音功能(可随时停止录音)
2020/10/26 Python
python 模拟登录B站的示例代码
2020/12/15 Python
详解pycharm的python包opencv(cv2)无代码提示问题的解决
2021/01/29 Python
HTML5组件Canvas实现图像灰度化(步骤+实例效果)
2013/04/22 HTML / CSS
财务会计毕业生自荐信
2013/11/02 职场文书
竞选班干部的演讲稿
2014/04/24 职场文书
民主生活会剖析材料
2014/09/30 职场文书
检察院起诉书
2015/05/20 职场文书
关于艺术节的开幕致辞
2016/03/04 职场文书
教你使用vscode 搭建react-native开发环境
2021/07/07 Javascript