本文发布在github.com/ssssyoki,欢迎star,issues共同交流。javascript
Koa是基于Node.js的下一代web开发框架,相比Express更轻,源码只有几百行。与传统的中间件不一样,在Koa 1.x中采用了generator实现中间件,这须要开发者熟悉ES6中的generator,Promise相关知识。css
在Koa官方文档示例代码中,采用yield next为跳转信号,而后会逆序执行中间件剩下的代码逻辑。这其中的逻辑很是有趣,本文将对其进行简要的分析。java
Koa的中间件跑在co模块下,而co能够将异步“变为”同步,从而实现用同步的方法写异步代码,避免了Node.js大量的回调嵌套。如今咱们从实现一个简易的co方法开始探索其中的机制。git
function co(generator){ let g = generator(); let next = function(data){ let result = g.next(data); if(result.done){ return ; }; if(result.value instanceof Promise){ result.value.then(function(d){ next(d); },function(err){ next(err); }); }else{ next(); }; }; next(); };
首先须要了解generator相关知识,接下来咱们分析这段代码: es6
首先定义一个参数为generator的co函数,当传入generator后(即app.use(function *(){...})
)定义next
方法实现对generator(能够理解为状态机)的状态遍历,因为每次遍历器指向新的yield
,返回结构如{value:'Promise','done':'true/false'}
的值,当done
的值为false
时遍历状态完毕并返回,若为true
则继续遍历。其中内部的g.next(data)
能够将上一个yield
的返回值传递给外部。同时,若generator中含有多个yield
且遍历未完成(即result.value
是Promise
对象 && result.done === false
),resolve()
所传递的数据能够在接下来then()
方法中直接使用,即递归调用,直到result.done === true
遍历结束并退出。github
这里可能存在一个疑惑,在第一次调用next()
方法时data为undefined
,那是否会致使error产生呢?其实V8引擎在执行时,会自动忽略第一次调用next()
时的参数,因此只有从第二次使用next()
方法时参数才是有效的。web
一言以蔽之,co实现了Promise递归调用generator的next方法。api
co-example.js
添加了测试代码,上述实例测试能够运行成功。理解了co的运行原理后,再来理解middleware的机制就容易多了。数组
middleware实现了所谓“逆序”执行,其实就是每次调用use()
方法时,将generator存入数组(记为s)中保存。在执行的时候先定义一个执行索引(记为index)和跳转标记(记为turn,也就是yield next
中的next
),再定义一个保存generator函数对象的数组(记为gs)。而后获取当前中间件generator,接着获取该generator的函数对象,将函数对象放在gs数组内保存,再执行generator的next()
方法。
执行开始后,根据返回的value
进行不一样的处理,若是是标记turn(即执行到了yield next
),说明该跳到下一个中间件了,此时令index++
,而后从数组g中获取下一个中间件重复上一个中间件的执行流程。app
当执行到的中间件没有yield
时,而且返回的done
为true
时,逆序执行。今后前用于保存generator函数对象的gs数组中取出上一个generator对象,而后执行generator的next()
方法,直到所有结束。
咱们打开Koa的application.js
文件:
/** * Use the given middleware 'fn'. * * @param {GeneratorFunction} fn * @return {Application} self * @api public */ app.use = function(fn){ if (!this.experimental) { // es7 async functions are not allowed, // so we have to make sure that 'fn' is a generator function assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function'); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; };
显而易见,app.use()
方法就是将generator传入this.middleware
数组中。其余部分的逻辑源码注释很是清晰,再也不赘述。
咱们再打开Koa-compose模块的index.js
文件:
/** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */ function compose(middleware){ return function *(next){ if (!next) next = noop(); var i = middleware.length; while (i--) { next = middleware[i].call(this, next); } return yield *next; } }
其中最关键的就是while
语句。将以前app.use()
传入并存储在middleware
中的generator逆序取出并执行,将每一个generator执行后的结果(即generator() === iterator)做为参数传入下一个(按数组的顺序则为前一个)generator中,在最后一个generator(数组第一个)执行后得出的next
变量(即第一个generator的iterator),执行yield *next
(即执行第一个generator的iterator)将所有generator像链表般串联起来。根据yield *
的特性,yield *next
将依次执行全部套用的next
(相似递归),从而造成所谓“正序执行再逆序执行”的流程。
从co到compose,代码只有短短几十行,但组合在一块儿却很是精巧奇妙,值得细细品味。