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

上述代码展示了 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()
}

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

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

(0)

相关推荐

  • 举例详解JavaScript中Promise的使用

    摘录 – Parse JavaScript SDK现在提供了支持大多数异步方法的兼容jquery的Promises模式,那么这意味着什么呢,读完下文你就了解了. "Promises" 代表着在javascript程序里下一个伟大的范式,但是理解他们为什么如此伟大不是件简单的事.它的核心就是一个promise代表一个任务结果,这个任务有可能完成有可能没完成.Promise模式唯一需要的一个接口是调用then方法,它可以用来注册当promise完成或者失败时调用的回调函数,这在Common

  • 详细解读JavaScript编程中的Promise使用

    Promise核心说明 尽管Promise已经有自己的规范,但目前的各类Promise库,在Promise的实现细节上是有差异的,部分API甚至在意义上完全不同.但Promise的核心内容,是相通的,它就是then方法.在相关术语中,promise指的就是一个有then方法,且该方法能触发特定行为的对象或函数. Promise可以有不同的实现方式,因此Promise核心说明并不会讨论任何具体的实现代码. 先阅读Promise核心说明的意思是:看,这就是需要写出来的结果,请参照这个结果想一想怎么用

  • 浅谈js promise看这篇足够了

    一.背景 大家都知道nodejs很快,为什么会这么快呢,原因就是node采用异步回调的方式来处理需要等待的事件,使得代码会继续往下执行不用在某个地方等待着.但是也有一个不好的地方,当我们有很多回调的时候,比如这个回调执行完需要去执行下个回调,然后接着再执行下个回调,这样就会造成层层嵌套,代码不清晰,很容易进入"回调监狱",就容易造成下边的例子: async(1, function(value){ async(value, function(value){ async(value, fu

  • 使用Promise解决多层异步调用的简单学习心得

    前言 第一次接触到Promise这个东西,是2012年微软发布Windows8操作系统后抱着作死好奇的心态研究用html5写Metro应用的时候.当时配合html5提供的WinJS库里面的异步接口全都是Promise形式,这对那时候刚刚毕业一点javascript基础都没有的我而言简直就是天书.我当时想的是,微软又在脑洞大开的瞎捣鼓了. 结果没想到,到了2015年,Promise居然写进ES6标准里面了.而且一项调查显示,js程序员们用这玩意用的还挺high. 讽刺的是,作为早在2012年就在M

  • 理解JavaScript中Promise的使用

    Javascript 采用回调函数(callback)来处理异步编程.从同步编程到异步回调编程有一个适应的过程,但是如果出现多层回调嵌套,也就是我们常说的厄运的回调金字塔(Pyramid of Doom),绝对是一种糟糕的编程体验.于是便有了 CommonJS 的 Promises/A 规范,用于解决回调金字塔问题.本文先介绍 Promises 相关规范,然后再通过解读一个迷你的 Promises 以加深理解. 什么是 Promise 一个 Promise 对象代表一个目前还不可用,但是在未来的

  • JavaScript Promise 用法

    同步编程通常来说易于调试和维护,然而,异步编程通常能获得更好的性能和更大的灵活性.异步的最大特点是无需等待."Promises"渐渐成为JavaScript里最重要的一部分,大量的新API都开始promise原理实现.下面让我们看一下什么是promise,以及它的API和用法! Promises现状 XMLHttpRequest API是异步的,但它没有使用promise API.但有很多原生的 javascript API 使用了promise: *Battery API *fetc

  • Javascript中的异步编程规范Promises/A详细介绍

    Javascript里异步编程逐渐被大家接受,先前大家一般通过回调嵌套,setTimeout.setInterval等方式实现,代码看起来非常不直观,不看整个代码逻辑很难快速理解.Javascript里异步函数大概有I/O函数(Ajax.postMessage.img load.script load等).计时函数(setTimeout.setInterval)等. 这些我们都很熟悉,在复杂的应用中往往会嵌套多层,甚至以为某些步骤未完成而导致程序异常,最简单的例子:比如你往DOM中注入节点,你必

  • JavaScript中的Promise使用详解

    许多的语言,为了将异步模式处理得更像平常的顺序,都包含一种有趣的方案库,它们被称之为promises,deferreds,或者futures.JavaScript的promises ,可以促进关注点分离,以代替紧密耦合的接口. 本文讲的是基于Promises/A 标准的JavaScript promises.[http://wiki.commonjs.org/wiki/Promises/A] Promise的用例: 执行规则 多个远程验证 超时处理 远程数据请求 动画 将事件逻辑从应用逻辑中解耦

  • 使用Promise链式调用解决多个异步回调的问题

    介绍 所谓Promise,简单来说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果. 缺少场景支撑,对于新手而言,很难理解Promise的意义. 在<你不知道的JavaScript中>有个场景介绍得很形象: 我走到快餐店的柜台,点了一个芝士汉堡.我交给收银员1.47美元.通过下订单并付款,我已经发出了一个对某个值(就是那个汉堡)的请求.我已经启 动了一次交易. 但是,通常我不能马上就得到这个汉堡.收银员会交给我某个东西来代替汉堡:一张带有 订单号的收据.订单号就是一个

  • koa源码中promise的解读

    koa 是一个非常轻量优雅的 node 应用开发框架,趁着双十一值班的空当阅读了下其源代码,其中一些比较有意思的地方整理成文与大家分享一下. 洋葱型中间件机制的实现原理 我们经常把 koa 中间件的执行机制类比于剥洋葱,这样设计其执行顺序的好处是我们不再需要手动去管理 request 和 response 的业务执行流程,且一个中间件对于 request 和 response 的不同逻辑能够放在同一个函数中,可以帮助我们极大的简化代码.在了解其实现原理之前,先来介绍一下 koa 的整体代码结构:

  • Android源码中final关键字的用法及final,finally,finalize的区别

    hi 大家好,今日,天气剧变,非常冷,不想出门,于是给大家写了篇文章,关于android final关键字及final,finally,finalize的区别相关知识,具体详情如下所示: 先预告一下,下文中仅涉及java语法的讨论,和Android源码关系不大,请不要有阅读压力. 我发现在Android的源码中很多地方对final关键字的用法很是"别出心裁",之所以这么说是因为我从没看过是这么使用final关键字的,一个典型的例子是View类中onScrollChanged方法(不妨将

  • Android源码中常用的接口传参实例详解

    Android源码中常用的接口传参实例详解 把MyCclass中的参数传到MyDclass /*接口传参例子2 * MyCclass.java发送MyDclass.java接收 * 原理和MyAclass.java发送MyDclass.java接收完全一样 * */ public class MyCclass { public void getEditext(GetMyFragmentData myFragmentData){ String edStr="人的生命是有限的,可是为人民服务是无限的

  • 详解Vue源码中一些util函数

    JS中很多开源库都有一个util文件夹,来存放一些常用的函数.这些套路属于那种常用但是不在ES规范中,同时又不足以单独为它发布一个npm模块.所以很多库都会单独写一个工具函数模块. 最进尝试阅读vue源码,看到很多有意思的函数,在这里分享一下. Object.prototype.toString.call(arg) 和 String(arg) 的区别? 上述两个表达式都是尝试将一个参数转化为字符串,但是还是有区别的. String(arg) 会尝试调用 arg.toString() 或者 arg

  • Vue源码中要const _toStr = Object.prototype.toString的原因分析

    在vue的源码中,vue/src/shared/util.js文件中存放的是一些方法.其中作者用了Object.prototype.toString这个方法来判断类型,但是并没有直接用,而是单独保存在一个变量: const _toStr = Object.prototype.toString 那么为什么要这么做呢? 先说下判断类型.众所周知,typeof在判断对象时不能正确判断Null,并且不能识别出Array,但在判断基础类型时是没问题的.所以尤大也写了: export function is

  • JDK源码中一些实用的“小技巧”总结

    前言 这段时间比较闲,就看起了jdk源码.一般的一个高级开发工程师, 能阅读一些源码对自己的提升还是蛮大的.本文总结了一些JDK源码中的"小技巧",分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 1 i++ vs i-- String源码的第985行,equals方法中 while (n--!= 0) { if (v1[i] != v2[i]) return false; i++; } 这段代码是用于判断字符串是否相等,但有个奇怪地方是用了i--!=0来做判断,我们通

  • 在Android系统源码中预置APK的方法

    如何将无源码的google play APK预置进系统(有源码和无源码有一点区别,网上下载的google play.apk解压之后里面没有源码)? (注意下文中的Test就是我们要预置到源码中的googleplay.apk的名字!!网上下载的Google Play Store 4.6.17.apk,名字中含有空格,将空格去掉或者自直接将它重新命名) 1.在 packages/apps 下面以需要预置的 APK 名字创建文件夹,以预制一个名为Test的APK为例  2.将 Test.apk 放到

  • 在Android源码中编译出指定jar包的操作

    今天想把android源码/vendor/letv/frameworks/base/java下的源码编译成 framework-letv.jar供乐乐语音客户端使用,编译完后,发现jar包文件虽然生成了,但包里面并没有相关的源码class文件,无法正常使用. 经过请教加研究发现,Android.mk文件需要添加选项如下: 54 LOCAL_JACK_ENABLED := disabled # important! 55 #include $(BUILD_JAVA_LIBRARY) 56 incl

  • HashMap源码中的位运算符&详解

    引言 最近在读HashMap源码的时候,发现在很多运算符替代常规运算符的现象.比如说用hash & (table.length-1) 来替代取模运算hash&(table.length):用if((e.hash & oldCap) == 0)判断扩容后元素的位置等等. 1.取模运算符%底层原理 ​总所周知,位运算&直接对二进制进行运算:而对于取模运算符%:a % b 相当于 a - a / b * b,底层实际上是除法器,究其根源也是由底层的减法和加法共同完成.所以其运行效

  • 设计模式系列之组合模式及其在JDK和MyBatis源码中的运用详解

    组合模式及其在JDK源码中的运用 前言组合和聚合什么是组合模式示例透明组合模式透明组合模式的缺陷安全组合模式 组合模式角色组合模式在JDK源码中的体现组合模式应用场景享元模式优缺点总结 前言 本文主要会讲述组合模式的用法,并会结合在JDK和MyBatis源码中的运用来进一步理解组合模式. 在编码原则中,有一条是:多用组合,少用继承.当然这里的组合和我们今天要讲的组合模式并不等价,这里的组合其实就是一种聚合,那么聚合和组合有什么区别呢? 组合和聚合 人在一起叫团伙,心在一起叫团队.用这句话来诠释组

随机推荐