原文地址javascript
最近几天花了比较长的时间在koa(1)的源码分析上面,初次看的时候,被中间件执行那段整的晕乎乎的,彻底不知道因此,再次看,好像明白了些什么,再反复看,我去,简直神了,简直泪流满面,简直丧心病狂啊!!!java
下面的例子会在控制台中打印出一些信息(具体打印出什么?能够猜猜😀),而后返回
hello world
。git
let koa = require('koa')
let app = koa()
app.use(function * (next) {
console.log('generate1----start')
yield next
console.log('generate1----end')
})
app.use(function * (next) {
console.log('generate2----start')
yield next
console.log('generate2----end')
this.body = 'hello world'
})
app.listen(3000)复制代码
用过koa的同窗都知道添加中间件的方式是使用koa实例的use
方法,并传入一个generator函数,这个generator函数能够接受一个next
(这个next究竟是啥?这里先不阐明,在后面会仔细说明)。程序员
执行use干了嘛github
这是koa的构造函数,为了没有其余信息的干扰,我去除了一些暂时用不到的代码,这里咱们把目光聚焦在middleware
这个数组便可。api
function Application() {
// xxx
this.middleware = []; // 这个数组就是用来装一个个中间件的
// xxx
}复制代码
接下来咱们要看use方法了数组
一样去除了一些暂时不用的代码,能够看到每次执行use方法,就把外面传进来的generator函数push到middleware数组中promise
app.use = function(fn){
// xxx
this.middleware.push(fn);
// xxx
};复制代码
好啦!你已经知道koa中是预先经过use方法,将请求可能会通过的中间件装在了一个数组中。app
接下来咱们要开始本文的重点了,当一个请求到来的时候,是怎样通过中间件,怎么跑起来的
koa
首先咱们只要知道下面这段callback
函数就是请求到来的时候执行的回调便可(一样尽可能去除了咱们不用的代码)
app.callback = function(){
// xxx
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));
// xxx
return function(req, res){
// xxx
fn.call(ctx).then(function () {
respond.call(ctx);
}).catch(ctx.onerror);
// xxx
}
};复制代码
这段代码能够分红两个部分
咱们分部分来讲一下
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));复制代码
这段代码对experimental
作了下判断,若是设置为了true
那么koa中将能够支持传入async函数,不然就执行co.wrap(compose(this.middleware))
。
只有一行初始化中间件就作完啦?
我知道koa很屌,但也别这么屌好很差,因此说评价一个好的程序员不是由代码量决定的
咱们来看下这段代码到底有什么神奇的地方
compose(this.middleware)复制代码
把装着中间件middleware
的数组做为参数传进了compose
这个方法,那么compose作了什么事呢?其实就是把本来毫无关系的一个个中间件给首尾串起来了,因而他们之间就有了千丝万缕的联系。
function compose(middleware){
return function *(next){
// 第一次获得next是因为*noop生成的generator对象
if (!next) next = noop();
var i = middleware.length;
// 从后往前开始执行middleware中的generator函数
while (i--) {
// 把后一个中间件获得的generator对象传给前一个做为第一个参数存在
next = middleware[i].call(this, next);
}
return yield *next;
}
}
function *noop(){}复制代码
文字解释一下就是,compose将中间件从最后一个开始处理,并一直往前直到第一个中间件。其中很是关键的就是将后一个中间件获得generator对象做为参数(这个参数就是文章开头说到的next啦,也就是说next实际上是一个generator对象
)传给前一个中间件。固然最后一个中间件的参数next
是一个空的generator函数生成的对象。
咱们本身来写一个简单的例子说明compose是如何将多个generator函数串联起来的
function * gen1 (next) {
yield 'gen1'
yield * next // 开始执行下一个中间件
yield 'gen1-end' // 下一个中间件执行完成再继续执行gen1中间件的逻辑
}
function * gen2 (next) {
yield 'gen2'
yield * next // 开始执行下一个中间件
yield 'gen2-end' // 下一个中间件执行完成再继续执行gen2中间件的逻辑
}
function * gen3 (next) {
yield 'gen3'
yield * next // 开始执行下一个中间件
yield 'gen3-end' // 下一个中间件执行完成再继续执行gen3中间件的逻辑
}
function * noop () {}
var middleware = [gen1, gen2, gen3]
var len = middleware.length
var next = noop() // 提供给最后一个中间件的参数
while(len--) {
next = middleware[len].call(null, next)
}
function * letGo (next) {
yield * next
}
var g = letGo(next)
g.next() // {value: "gen1", done: false}
g.next() // {value: "gen2", done: false}
g.next() // {value: "gen3", done: false}
g.next() // {value: "gen3-end", done: false}
g.next() // {value: "gen2-end", done: false}
g.next() // {value: "gen1-end", done: false}
g.next() // {value: undefined, done: true}复制代码
看到了吗?中间件被串起来以后执行的顺序是
gen1 -> gen2 -> gen3 -> noop -> gen3 -> gen2 -> gen1
从而首尾相连,进而发生了关系😈。
经过compose处理后返回了一个generator函数。
co.wrap(compose(this.middleware))复制代码
全部上述代码能够理解为
co.wrap(function * gen ())复制代码
好,咱们再看看co.wrap
作了什么,慢慢地一步步靠近了哦
co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
}复制代码
能够看到co.wrap
返回了一个普通函数createPromise
,这个函数就是文章开头的fn
啦。
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));复制代码
前面已经说完了,中间件是如何初始化的,即若是由不相干到关系密切了,接下来开始说请求到来时,初始化好的中间件是怎么跑的。
fn.call(ctx).then(function () {
respond.call(ctx);
}).catch(ctx.onerror);复制代码
这一段即是请求到来手即将要通过的中间件执行部分,fn执行以后返回的是一个Promise,koa经过注册成功和失败的回调函数来分别处理请求。
让咱们回到
co.wrap = function (fn) {
// xxx
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
}复制代码
createPromise
里面的fn就是通过compose处理中间件后返回的一个generator函数,那么执行以后拿到的就是一个generator对象了,并把这个对象传经经典的co里面啦。若是你须要对co的源码了解欢迎查看昨天写的走一步再走一步,揭开co的神秘面纱,好了,接下来就是看co里面如何处理这个被compose处理过的generator对象了
再回顾一下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);
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) + '"'));
}
});
}复制代码
咱们直接看一下onFulfilled
,这个时候第一次进co的时候由于已是generator对象因此会直接执行onFulfilled()
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}复制代码
而gen.next
正是用于去执行中间件的业务逻辑,当遇到yield语句的时候,将紧随其后的结果返回赋值给ret
,一般这里的ret,就是咱们文中说道的next
,也就是当前中间件的下一个中间件。
拿到下一个中间件后把他交给next
去处理
function next(ret) {
if (ret.done) return resolve(ret.value);
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) + '"'));
}复制代码
当中间件执行结束了,就把Promise的状态设置为成功。不然就将ret
(也就是下一个中间件)再用co包一次。主要看toPromise
的这几行代码便可
function toPromise(obj) {
// xxx
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
// xxx
}复制代码
注意噢toPromise
这个时候的返回值是一个Promise,这个很是关键,是下一个中间件执行完成以后回溯到上一个中间件中断执行处继续执行的关键
function next(ret) {
// xxx
var value = toPromise.call(ctx, ret.value);
// 即经过前面toPromise返回的Promise实现,当后一个中间件执行结束,回退到上一个中间件中断处继续执行
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// xxx
}复制代码
看到这里,咱们能够总结出,几乎koa的中间件都会被co给包装一次,而每个中间件又能够经过Promise的then去监测其后一个中间件是否结束,后一个中间件结束后会执行前一个中间件用then监听的操做,这个操做即是执行该中间件yield next后面的那些代码
打个比方:
当koa中接收到一个请求的时候,请求将通过两个中间件,分别是中间件1
和中间件2
,
中间件1
// 中间件1在yield 中间件2以前的代码
yield 中间件2
// 中间件2执行完成以后继续执行中间件1的代码复制代码
中间件2
// 中间件2在yield noop中间件以前的代码
yield noop中间件
// noop中间件执行完成以后继续执行中间件2的代码复制代码
那么处理的过程就是co会当即调用onFulfilled来执行中间件1前半部分代码,遇到yield 中间件2
的时候获得中间件2generator对象,紧接着,又把这个对象放到co里面继续执行一遍,以此类推下去知道最后一个中间件(咱们这里的指的是那个空的noop中间件)执行结束,继而立刻调用promise的resolve方法表示结束,ok,这个时候中间件2监听到noop执行结束了,立刻又去执行了onFulfilled来执行yield noop中间件后半部分代码,好啦这个时候中间件2也执行结束了,也会立刻调用promise的resolve方法表示结束,ok,这个时候中间件1监听到中间件2执行结束了,立刻又去执行了onFulfilled来执行yield 中间件2后半部分代码,最后中间件所有执行完了,就执行respond.call(ctx);
啊 啊 啊好绕,不过慢慢看,仔细想,仍是能够想明白的。用代码表示这个过程有点相似
new Promise((resolve, reject) => {
// 我是中间件1
yield new Promise((resolve, reject) => {
// 我是中间件2
yield new Promise((resolve, reject) => {
// 我是body
})
// 我是中间件2
})
// 我是中间件1
});复制代码
罗里吧嗦说了一大堆,也不知道有没有把执行原理说明白。
若是对你理解koa有些许帮助,不介意的话,点击源码地址点颗小星星吧
若是对你理解koa有些许帮助,不介意的话,点击源码地址点颗小星星吧
若是对你理解koa有些许帮助,不介意的话,点击源码地址点颗小星星吧