其实koa源码就4个文件,文件目录还是挺清爽的。
主文件为 lib/application.js request.js,response.js都是为context服。有兴趣的同学可以去仔细看看,这里我简单抽出源码主要部分,以小例子的形式分析一波,总结两个点:“洋葱模型”和“context上下文”
koa的洋葱模型
每次koa实例use(fn),形成一个队列[fn1,fn2,fn3],然后依次执行,下个函数是否执行由上一个函数中的next参数控制,其实就是当前函数fn1执行后,把下一个函数fn2赋值给f1的next参数,这样就形成了一个洋葱模型
小例子代码如下,可以先跳过回来看:
class koa {
constructor () {
this.middlewares = []
}
use (fn) {
this.middlewares.push(fn)
}
compose (middlewares) {
let index = -1
dispatch(0)
function dispatch (i) {
if (i <= index) {
console.log('同一个use重复调用多次了')
return
}
index = i
let fn = middlewares[i]
if (i === middlewares.length) fn = () => {console.log('你最后一步 调用了next,走到洋葱中心了')}
fn(dispatch.bind(null, i + 1))
}
}
listen () {
this.compose(this.middlewares)
}
}
我们一步一步分析
主要逻辑在 compose函数里面,以下建立了一个koa类,定义了 middlewares中间件数组,use 函数,compose函数,listen函数
class koa {
constructor () {
this.middlewares = []
}
use (fn) {
this.middlewares.push(fn)
}
compose (middlewares) {
...
}
listen () {
this.compose(this.middlewares)
}
}
最主要来讲 compose
其实就是一个递归执行的同时将下一个函数传递给当前执行函数的next参数
这样,是否执行下一个函数就是由当前next参数来决定
compose (middlewares) {
dispatch(0)
function dispatch (i) {
let fn = middlewares[i]
if (i === middlewares.length) fn = () => {console.log('你最后一步 调用了next,走到洋葱中心了')}
fn(dispatch.bind(null, i + 1))
}
}
处理最后一个函数还在执行next情况
compose (middlewares) {
dispatch(0)
function dispatch (i) {
let fn = middlewares[i]
/**************新增这里**************/
if (i === middlewares.length) fn = () => {console.log('你最后一步 调用了next,走到洋葱中心了')}
/***************************/
fn(dispatch.bind(null, i + 1))
}
}
不允许同一个函数,执行两遍以上next
compose (middlewares) {
/**************新增这里**************/
let index = -1
/****************************/
dispatch(0)
function dispatch (i) {
/**************新增这里**************/
if (i <= index) {
console.log('同一个use重复调用多次了')
return
}
index = i
/****************************/
let fn = middlewares[i]
if (i === middlewares.length) fn = () => {console.log('你最后一步 调用了next,走到洋葱中心了')}
fn(dispatch.bind(null, i + 1))
}
}
以上就是全过程,其实就是koa源码中依赖的koa-compose库(就一个函数,也是koa作者写的),我这里少了很多判断,简化
去看源码的同学会发现,他最后的输出是Promise,其实并不影响整个逻辑。看懂这里其实就掌握了所谓的“洋葱模型”
context上下文
主要解析:为什么ctx.req === ctx.request.req -> true?
其实就是koa所依赖的delegates里面逻辑,也是koa作者维护的,也只有一个文件
这里把他抽出来,简单写了一个小例子如下:
function delefates (context, target) {
if (!(this instanceof delefates)) return new delefates(context, target)
this.context = context
this.target = target
}
delefates.prototype.method = function (name) {
// 当前this指向 delefates(实例化对象
let context = this.context
let target = this.target
context[name] = function () {
// 当前this指向 使用对象(context)
return this[target][name].apply(this[target], arguments)
}
return this
}
delefates.prototype.getter = function (name) {
let context = this.context
let target = this.target
this.__defineGetter__(name, function () {
return this[target][name]
})
return this
}
一步一步分析
定义了delefates函数:
1.如果当前函数直接调用,则返回当前函数实例化对象
2.将传入的参数赋值到函数实例化属性上
function delefates (context, target) {
if (!(this instanceof delefates)) return new delefates(context, target)
this.context = context
this.target = target
}
consol.log(delefates({aaa: 111}).obj.aaa) // 111
接下来,模拟了delefates函数 method和getter方法
method 里面的逻辑主要是一个this指向问题和一个小闭包
第一个this所在逻辑为:存储下target代理层属性为闭包使用,定义context对象[name]方法
第二个this所在逻辑为:在方法被调用时,透层调用 context下的target对象name方法,使用apply改变target[name]方法作用域上下文对象
delefates.prototype.method = function (name) {
// 当前this指向 delefates(实例化对象
let context = this.context
let target = this.target
context[name] = function () {
// 当前this指向 使用对象(context)
return this[target][name].apply(this[target], arguments)
}
return this
}
// 使用
let context = {
request: {
req: () => {}
}
}
delefates(context,'request')
.method('req')
/** 结果
context = {
request: {
req: () => {}
},
req = function () {
// 当前this指向 使用对象(context)
return this[target][name].apply(this[target], arguments)
}
}
**/
getter
使用了“废弃版” Object.defineProperties
Object.defineGetter:透层代理传入属性的属性
delefates.prototype.getter = function (name) {
let context = this.context
let target = this.target
this.__defineGetter__(name, function () {
return this[target][name]
})
return this
}
// 使用
let context = {
request: {
a: 1
}
}
delefates(context,'request')
.getter('a')
context.a // 1
总结
了解了kao的洋葱模型原理以及context上下文对象属性绑定过程,最后以小例子的形式总结,加油勉励!