几行代码讲koa源码

158 阅读3分钟

其实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上下文对象属性绑定过程,最后以小例子的形式总结,加油勉励!