You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// compose 例子varcompose=require('composition');varstack=[];stack.push(function(next){console.log('first');returnnext.then(function(){console.log('first is ok');})});stack.push(function(){console.log('second');returnnewPromise(function(resolve){setTimeout(function(){console.log('second is ok');resolve()});});});// 被 composition 组装之后的样子,它的**执行结果**为一个 promise。compose(stack)().then(function(val){console.log(' ---- final -----');});
functionco(gen){varctx=this;varargs=slice.call(arguments,1)returnnewPromise(function(resolve,reject){if(typeofgen==='function')gen=gen.apply(ctx,args);if(!gen||typeofgen.next!=='function')returnresolve(gen);// 启动 generatoronFulfilled();functiononFulfilled(res){varret;try{ret=gen.next(res);}catch(e){returnreject(e);}// 将执行结果传入 nextnext(ret);}// 异常处理代码略掉...functionnext(ret){// 根据 ret 的各种情况进行分别处理。// 如果迭代器已经执行完毕,则通过 Promise 的 resove 函数触发改变 Promise 的状态。if(ret.done)returnresolve(ret.value);// 将迭代器上一步的返回值进行 promise 化处理。varvalue=toPromise.call(ctx,ret.value);if(value&&isPromise(value))returnvalue.then(onFulfilled,onRejected);returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '+'but the following object was passed: "'+String(ret.value)+'"'));}});
简介
Koa 是一个非常小巧且轻量级的由 Node.js 完成的 web框架,底层借助于 co 解决了繁琐的回调嵌套,充分发挥了 Generator 的优势。目前该 team 已经尝试将 async/await 引入其中,不过还在 Alpha 阶段。
关于 Koa,还有一个关键字:Middleware。
这篇文章主要是在边看 Koa 源码边做的一个整理笔记。
如何使用
先通过代码看一段简单示例。
如果细心看这段代码的话,对代码有一些直觉的同学到此就应该能大致了解一些,比如 app.use 中的函数会一个个执行,执行的方式大概是利用
yield next
触发的吧,next
那大概应该指向的是下一个中间件吧?没错,是这样的。但是这就有一个疑问,如果
next
指向的是下一个中间件,那 next 的类型是什么,应该还是和 Generator 有关的吧,比如 Generator函数?比如 Iterator Object?也没错。那可是在 Generator 内部,如果
yield
后面跟的还是 Generator,根据规范,这样是不会执行到内部 Generator 的吧?恩,那 Koa 是怎么做到的呢?这个就当做 谜1 吧。
看完这篇文章就有答案了。
源码分析
考虑到篇幅,后续文中出现的源码会删掉不重要且不影响关键逻辑部分的代码,比如调试信息。
Koa 部分
通过上面的实例结合 koa 的两个最重要的 api 看源码。入口文件 lib/application.js。
原型链上有两个比较重要的 API,也是经常会看到的。
listen 方法很简短,源码备注已经说的很明白了,就是
的简写,但这里有个方法需要后续了解一下,就是
Application#callback
,这个放在Application#use
之后讲,一步步来,先看Application#use
。Application#use 方法源码:
现在来看
Application#callback
。按从上到下,应该先看 Koa 是如何处理中间件的。但是这里需要投入大量的篇幅,而另外一个问题中间件执行完之后 respond 的源码实现相对来说就简短多了,只需要大概撇一眼即可,所以这里先看 respond 的实现,接下来再重点看 Koa 对中间件的处理。
现在,重头戏才刚刚开始。这也是我认为的 koa 最有价值的地方,看看 koa 是如何处理这些中间件的。
兵分两路,先看 compose_es7 的方式。但是在这之前,需要先介绍一个概念,compose。
Compose
通俗得来说 Compose 做的事情就是将多个函数组合成一个函数的过程。这里借用 Ramda 库的 compose 官方示例来说明它的作用。
试想一下要完成这一个需求场景如果在不用 R.compose 库的时候会是什么样子。
再试想一下在经常可以看到的业务场景之下的对比,对一串 JSON 做一些处理,比如根据 id 排序,根据日期过滤,摘取其中的 content 和 id 字段等等。
借助 compose
不借助 compose,怎么写呢?这该用多少行代码才能做到并且还要在满足可复用性的前提下。
这样子的处理有什么好处呢?后者,数据混淆在逻辑之中,很难做到两者的分离,对于再复杂一点的业务来说,更是增加了抽象的难度。
而先通过 compose 的形式组合出一套完整的公式再等待数据的输入更有利于处理逻辑和数据的分离。这其实正是体现了函数式编程的思想所在:先有公式,再把数据塞给公式处理,返回结果,而不是算好一个值,再给另一个公式。公式有一个简单,那公式有多个怎么办,compose 一下变成一个。
这也是这里没有用 underscore 中 compose 的原因,underscore 虽然是函数式,但它总需要先传入数据,然后再有函数的链式调用,它更像是以容器为中心对操作的串联,更像是 jQuery 的变形,这个和真正的函数式有点脱节。
如果你有看过 Redux 源码的话,不知道有没有留意到 Redux 中
applyMiddleWare
部分也是借用了 compose 这一理念来实现的,这一部分,其实又和 Koa 是类似的。把一个个的中间件,经过 compose 组合成一条流水线(一个函数),然后将需要处理的数据传入这条流水线,顺次加工处理。Composition 源码分析
现在回到主题,继续看 Koa。
Koa 中
compose_es7
依赖于另一个库完成的: composition,主要的任务是借助 compose 的思想将多个函数组合成一个,废话嘛,当然这不是该库存在的意义,它的真正价值在于对异步(比如Generator, Async/Await,Promise)的支持。。 代码量也不多,一共151行代码,它的执行结果最终返回的是既支持 Promise 也支持 Generator 的对象,这个在 koa 的源码中也可以看出来。实现看代码,这里只看 Promise 的处理方式,其他的类似。通过结合使用例子来看源码,先例子。
再源码。
对 Generator 的处理大同小异吧,这里略过。正是因为源码中多了 Promise 和 Generator 的互转,所以完成了同时满足这两种异步编程风格的使命。
co 是如何实现的
细心的人可能会发现,Composition 源码部分
_getPromise
也有用到co
,承担了把 Generator 转化为 Promise 的功能。那在 koa 中呢?还是看这行代码
那就从
co.wrap(compose(this.middleware))
开始看起吧。co.wrap
可以将一个 generator 形式转化为 promise。在这里先对 middleware 做了组合处理,翻翻compose
的源码。单独看这段代码,还不能充分理解,结合一段示例代码来充分消化 compose 的妙处。
当 fn 开始执行时,会从第一个 Generator 生成的 Iterator Object 开始执行,并带有 next 参数。就像拨洋葱一样,一层一层深入,再一层层出来,回忆一下 Koa 官网的那个配图。
插播关于 yield 和 yield*
compose 的最后一行
yield* prev
和yield prev
区别是什么呢?从概念上就可以看出两者的区别:如果需要在 Generator 函数 内部,调用另一个 Generator 函数,则必须使用
yield*
,否则没有效果。换句话说,yield*
等于在外层的 Generator 函数内部,部署了内层 Generator 函数的 for of 循环。看例子。
Ok,在回到主线任务前,还有一个问题,还没展开,就是
Koa 官网中的中间件使用的方式还记得吗?
而在我刚才的示例代码中是
前者使用的是
yield
(前者也可以使用 yield_),后者必须使用yield_
这是为什么?这个问题当做 谜2 放在 co 的源码分析完之后自会有答案。现在回头再看,发现
compose
其实和上面composition
差不了太多,区别比较大的地方是是这里返回的是 Generator。返回后的 Generator 交给 co.wrap 进一步处理。真正的
co
函数长什么样子,打开这个传颂很久的神秘盒子看看。上一步中,对迭代器每一步的返回值都进行
toPromise
转变。看到这里是不是有点拨开云雾见天日的感觉呢。在对 obj 做 toPromise 的转变时,如果 obj 为 Generator,那就再 obj 上再调用一次 co,co 函数内部会执行这个 Generator,并通过返回的 promise 跟踪状态,触发相关逻辑(回想上面的 co 函数源码)。对外部的好处,统一了编程风格,自然也就不用再去 care yield 关键字后面到底是个异步任务呢,还是个 Generator 呢。
谜1和谜2 的疑团是不是解开了呢。不过话说回来,多一次的调用,自然就多出一点额外的性能损耗,如果追求极致的性能,那就明确指名使用
yield* next
,如果需要统一的风格,那大可以直接用yield next
的形式。via: promoteyield* next
instead ofyield next
结语
到这里,Koa 的源码基本粗略得过完了,肯定还有一些比较细节但处理比较好的地方尚未顾及到,等再有机会 review 时再做补充吧。
有没有发现什么值得学习借鉴的地方,或者好的思想,有,三点。
参考资料
yield* next
instead ofyield next
The text was updated successfully, but these errors were encountered: