Koa(五、源码浅析)

基础http请求

const http=require("http");
const app=http.createServer((req,res)=>{
    res.writeHead(200);
    res.end("Hello");
})
app.listen(3000,()=>{
    console.log("listen to 3001")
})

针对http基础请求简单封装

Application.js:

const http=require('http');
class Application{
    constructor(){
        this.callback=''
    }
    use(callback){
        this.callback=callback
    }
    listen(...args){
        let app=http.createServer((req,res)=>{
            this.callback(req,res);
        })
        app.listen(...args)
    }
}
module.exports=Application


test.js:

const Qow=require('./Application');
const app=new Qow();
app.use((req,res)=>{
    res.writeHead(200);
    res.end("Hello");
}) 
app.listen(3001,()=>{
    console.log("listen to 3001")
})

效果上面两者相同,下面继续封装

js的getter和setter

const zq={
    _name:"zq",
    get name(){
        return this._name
    },
    set name(name){
        this._name=name
    }
}
console.log(zq.name)
zq.name="zq1"
console.log(zq.name)
说明:类似于java的get,set,其实_name还可以使用,
只不过规范定义为下划线标志为私有,最好别使用。

封装ctx的阶段性代码

Application.js:
const http = require("http")
let response = {
    get body() {
        return this._body;
    },
    set body(val) {
        this._body = val
    }
}
let request = {
    get url() {
        return this.req.url;
    }
}
let context = {
    get body() {
        return this.response.body;
    },
    set body(val) {
        this.response.body = val;
    },
    get url() {
        return this.request.url;
    }
}
class Application {
    constructor() {
        this.context = context
        this.request = request
        this.response = response
    }
    use(callback) {
        this.callback = callback;
    }
    listen(...args) {
//作用:async作用是因为内部的callback可能是异步操作
        let app = http.createServer(async (req, res) => {
            let ctx = this.createCtx(req, res)
            await this.callback(ctx);
//在callbak内部,也就是test.js中ctx.body已经被重新赋值,所以下面可以直接使用
            ctx.res.end(ctx.body)
        })
        app.listen(...args)
    }
    createCtx(req, res) {
        let ctx = Object.create(this.context);
        ctx.request = Object.create(this.request);
        ctx.response = Object.create(this.response)
 //作用:就是类似koa的ctx直接可使用一些属性,同时request/response也都可以使用
        ctx.req = ctx.request.req = req;
        ctx.res = ctx.response.res = res;
        return ctx;
    }
}
module.exports = Application

test.js:
const Qow=require('./demo/a');
const app=new Qow();
app.use(async ctx=>{
    ctx.body='Hello'
}) 
app.listen(3001,()=>{
    console.log("listen to 3001")
})

说明:Obejct.create作用就是复制一份一样的,因为上面的context等都是在类外面,使用时候导入在内存当中只有一份,每次new对象的时候,都是独立的,所以需要复制一份在当前的对象中

嵌套调用

function add(x,y) {
    return x+y
}
function double(z){
    return z*2
}
const result=double(add(1,2))
console.log(result)//6

同步中间件模拟

function add(x,y) {
    return x+y
}
function double(z){
    return z*2
}
const middlewares=[add,double];
let len=middlewares.length;
function compose(midds){
    return (...args)=>{
        //初始化默认执行第一个函数(后期就是中间件)
        let res=midds[0](...args)
        for (let i = 1; i < len; i++) {
            res=midds[i](res) 
        }
        return res;
    }
}
const fn=compose(middlewares)
const result=fn(1,2)
console.log(result)//6
说明:该逻辑和上面的js模块效果相同都是串联执行,把上一次结果当成下一次的参数

中间件模拟

//异步的测试
async function fn1(next){
    console.log('fn1');
    await next()
    
    console.log('end fn1');
}
async function fn2(next){
    console.log('fn2');
    await delay()
    await next()
    console.log('end fn2');
}
function fn3(next){
    console.log('fn3');
}
function delay(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{
            resolve()
        },2000)
    })
}
function compose(middlewares){
    return function () {
        return dispatch(0)
        function dispatch(i){
            let fn=middlewares[i];
            if(!fn){
                return Promise.resolve()
            }
            return Promise.resolve(fn(function next() {
                return dispatch(i+1)
            }))
        } 
    }
}
const middlewares=[fn1,fn2,fn3];
const final=compose(middlewares);
final()
输出:输出 fn1 fn2 延时两秒 fn3 end fn2 end fn1

说明:核心就是compose方法,Promise.resolve作用就是立即执行该方法,然后返回一个Promise
if里面是判断条件,如果中间件已经递归完毕则退出并返回一个promise,fn就是中间件,fn内部回调
是next函数,所以如果在调用时候,如果next不手动调用,则不进入下一个中间件,因为dispatch不会
被继续递归调用。
如果把fn1中next的await去掉,输出就是fn1 fn2 延时两秒 fn3 end fn1 end fn2
async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到
await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
next其实就是返回的dispatch就是一个promise,如果不加await就会立即执行完毕然后返回promise,
根据下图可知,fn0输出之后next相当于内部调用dispatch(1),然后其内部next相当于调用dispatch(2).
没有加await在js的事件轮询里面,就不会等待回调执行完毕才开启执行,执行所有next之前的顺序执行完毕之后,因为调用栈的原因,回调肯定靠后执行,此时没加await的next后面的语句会先执行完,然后下次轮询才会从回调队列找出回调的依次执行。

中间件洋葱图模拟.png

精简版本Koa实现和测试

Application.js:

const http = require("http")
let response = {
    get body() {
        return this._body;
    },
    set body(val) {
        this._body = val
    }
}
let request = {
    get url() {
        return this.req.url;
    }
}
let context = {
    get body() {
        return this.response.body;
    },
    set body(val) {
        this.response.body = val;
    },
    get url() {
        return this.request.url;
    }
}
class Application {
    constructor() {
        this.context = context
        this.request = request
        this.response = response
        this.middlewares=[]
    }
    use(callback) {
        this.middlewares.push(callback)
        // this.callback = callback;
    }
    listen(...args) {
        let app = http.createServer(async (req, res) => {
            let ctx = this.createCtx(req, res)
            const fn=this.compose(this.middlewares)
            await fn(ctx);
            ctx.res.end(ctx.body)
        })
        app.listen(...args)
    }
    compose(middlewares){
        return function (context) {
            return dispatch(0)
            function dispatch(i){
                let fn=middlewares[i];
                if(!fn){
                    return Promise.resolve()
                }
                return Promise.resolve(fn(context,function next() {
                    return dispatch(i+1)
                }))
            } 
        }
    }
    createCtx(req, res) {
        let ctx = Object.create(this.context);
        ctx.request = Object.create(this.request);
        ctx.response = Object.create(this.response);
        ctx.req = ctx.request.req = req;
        ctx.res = ctx.response.res = res;
        return ctx;
    }
}
module.exports = Application

test.js:
const Qow=require('./demo/a');
const app=new Qow();
app.use(async (ctx,next)=>{
    ctx.body='1'
    await next()
    ctx.body+='5'
})
app.use(async (ctx,next)=>{
    ctx.body+='2'
    await next()
    ctx.body+='4'
}) 
app.use(async ctx=>{
    ctx.body+='3'
}) 
app.listen(3001,()=>{
    console.log("listen to 3001")
})
网页输出:12345

next浅析

Koa 到了2.x,代码越发精简了,基本的思想还是一样的,依然是缓存中间件并使用compose 进行串联,只是中间件参数从一个next 变成了(ctx, next),且中间件再不是generator函数而是一个 async/await 函数了

  use(fn) {
    // ...
    this.middleware.push(fn);
    return this;
  }
  // ...
  callback() {
    const fn = compose(this.middleware);
    // ..
  }

同时, compose 的实现也变了,相较于1.x 显得复杂了一些,用了四层return,将关注点放在dispatch 函数上:

function compose (middleware) {
  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

可以把注意力放到dispatch.bind上面,bind代表绑定但是不执行,其实就是在编写代码时候对应的await next(),
因为Promise.resolve(fn(context, dispatch.bind(null, i + 1)));的递归调用,然后bind的绑定但是不执行,其实大概逻辑
就如下面一段代码,而Promise.resolve()其实是把对象包装成Promise,可知,下面一段代码的await就是await next前面的
await。

神来之笔在于Promise.resolve(fn(context, dispatch.bind(null, i + 1)))这一句,乍看一下有点难懂,实际上fn(context, dispatch.bind(null, i + 1)) 就相当于一个中间件,然后递归调用下一个中间件,我们从dispatch(0) 开始将它展开:

// 执行第一个中间件 p1-1
Promise.resolve(function(context, next){
  console.log('executing first mw');
  // 执行第二个中间件 p2-1
    await Promise.resolve(function(context, next){
    console.log('executing second mw');
    // 执行第三个中间件 p3-1
        await Promise(function(context, next){
      console.log('executing third mw');
      await next()
      // 回过来执行 p3-2
      console.log('executing third mw2');
    }());
    // 回过来执行 p2-2
        console.log('executing second mw2');
  })
  // 回过来执行 p1-2
    console.log('executing first mw2'); 
}());

执行顺序可以理解为以下的样子:

// 执行第一个中间件 p1-1
first = (ctx, next) => {
  console.log('executing first mw');
  next();
  // next() 即执行了第二个中间件 p2-1
  second = (ctx, next) => {
    console.log('executing second mw');
    next();
    // next() 即执行了第三个中间件 p3-1
    third = (ctx, next) => {
      console.log('executing third mw');
      next(); // 没有下一个中间件了, 开始执行剩余代码
      // 回过来执行 p3-2
      console.log('executing third mw2');
    }
    // 回过来执行 p2-2
    console.log('executing second mw2');
  }
  // 回过来执行 p1-2
  console.log('executing first mw2'); 
} 

从上面我们也能看出来,如果我们在中间件中没有执行 await next() 的话,就无法进入下一个中间件,导致运行停住。在2.x 中,next 不再是generator,而是以包裹在Promise.resolve 中的普通函数等待await 执行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,858评论 25 707
  • #第十六章-第二十六章 「痛苦使人高尚这种说法并不符合事实,幸福偶尔会使人高尚,但至于痛苦,在大多数情况下,只会使...
    小鹿大侠阅读 203评论 0 0
  • 今天的学习素材是乔希·维茨金的《学习之道》分享了三大学习的秘籍,分别是渐进理论,软区域和漩涡效应。 1,渐进理论。...
    古林阅读 254评论 0 0
  • 今天是回到家的第三天!家,还是曾经的样子,变的是我!曾几何,我的世界都存在于这个地方!现如今,这里的一切像是...
    Hallo小先生阅读 276评论 1 0
  • 我是你指间 滑落的一粒流沙 混在光阴的队伍里远赴天涯 此时,时间的脚步慢了下来 片片白云飘过山顶 老井睁着凹陷的眼...
    张珍艺阅读 421评论 5 4