Skip to content

koa1 #10

Description

@chengfengliu

koa 中间件机制实现原理

一个简单的co实现

function co(fn) {
    return function(done) {
        var ctx = this;
        var gen = fn.call(ctx);
        var it = null;
        function _next(err, res) {
            if(err) res = err;
            it = gen.next(res);
            //{value:function(){},done:false}
            if(!it.done){
                it.value(_next);
            }
        }
        _next();
    }
}

调用

var fs = require('fs');
//一个 thunk 函数
function read(file) {
    return function(fn){
        fs.readFile(file, 'utf8', fn);
    }
}
co(function *(){
    var c = 2;
    console.log(c);
    var a = yield read('error.js');
    console.log(a.length);

    var b = yield read('package.json');
    console.log(b.length);
})();

生成器(*)函数调用只会返回迭代器,不会执行函数体,只有迭代器.next()才会从头开始按步骤执行生成器函数体。所以fn.call(ctx)并不会打印c
第一次调用 _next() ,执行到 gen.next(res),执行完 read('error.js') 后,执行 it 的赋值是

{value:function(fn){
   fs.readFile('error.js', 'utf8', fn);
},done:false}

然后执行it.value(_next),即fs.readFile('error.js', 'utf8', _next),_next为fs.readFile的回调函数,即读取文件后才执行的回调函数,当读取error.js完毕后再次执行_next。
我们都知道node的fs函数其回调函数函数的参数都是(err, result),所以_next同样如此,所以_next执行到gen.next(res)时res就是读取文件的结果,这时因为var a = yield read('error.js');
所以a得到了文件数据。

关键,read函数是thunk偏函数,执行完返回一个函数,所以it.value是一个函数,才能传入_next参数继续调用
Thunk函数是自动执行Generator函数,如果没有Thunk

function _next() {
  if(!it.done) {
    it.next()
    _next()    
  }
}

这样就不能把读取的文件数据传给fs.readFile函数外的变量a,因为只有fs.readFile(filename, (err,result) => {})的回调函数参数才能读取其参数result,所以想要得到result,就要回调函数把result从自身函数体内传出来。
怎么传出来呢?你可能想定义一个全局变量,然后在回调函数体内把结果result赋值给它。这样不行,因为主线程不知道异步线程什么时候完成读取文件工作并把result赋值给这个全局变量。这样造成你的全局变量在主线程使用时可能是undefined
答案是用在回调函数内迭代器.next(result),等我做好所有工作了再传出来

例如,没用Thunk

const fs = require('fs')
function cb(err,result) {
    if(err) return
    it.next(result)
}

function *generator() {
    let a = yield fs.readFile('package.json','utf8', cb)
    console.log(a)
    let b = yield fs.readFile('package.json','utf8', cb)
    console.log(b)
    let c = yield 1
    // 下面一句没有执行
    console.log(c)
}
let it = generator()
// 开始执行
it.next()

这时需要一个任务执行器,

const fs = require('fs')
function cb(err,result) {
    if(err) return
    // console.log(result)
}

function *generator() {
    let a = yield fs.readFile('package.json','utf8', cb)
    console.log('a',a)
    let b = yield fs.readFile('package.json','utf8', cb)
    console.log('b',b)
    let c = yield 1
    console.log('c',c)
}
function run(taskDef) {
    let it = taskDef()
    let obj = it.next()
    function step() {
        if(!obj.done) {
            obj = it.next(obj.value)
            step()
        }
    }
    step()
}
run(generator)

但是a和b并没有接收到数据

这也是Thunk的作用体现出来了,任务执行器根据obj的value是否为函数(Thunk)来判断执行回调函数(回调函数决定什么时候执行下一个yield)还是直接执行下一个yield

const fs = require('fs')
function readFile() {
    return function(callback) {
        fs.readFile('package.json', 'utf8', callback)
    }
}
function *generator() {
    console.log('start')
    let a = yield readFile()
    console.log('a', a)
    let b = yield readFile()
    console.log('b', b)
    let c = yield 1
    console.log(c)
}
function run(taskDef) {
    let it = taskDef()
    let obj = it.next()
    function step() {
        if(!obj.done) {
            if(typeof result.value === 'function') {
                result.value((err, result) => {
                    obj = it.next(result)
                    step()
                })
            } else {
                obj = it.next(obj.value)
                step()
            }
        }
    }
    step()
}
run(generator)

实现简单的 koa 框架(剥离除中间件外所有的逻辑)

var co = require('co');

function SimpleKoa(){
    this.middlewares = [];
}
SimpleKoa.prototype = {
    //注入个中间件
    use: function(gf){
        this.middlewares.push(gf);
    },
    //执行中间件
    listen: function(){
        this._run();
    },
    _run: function(){
        var ctx = this;
        var middlewares = ctx.middlewares;
        return co(function *(){
            // 最后一个中间件没有yield语句,所以可以其参数next的值可以为null
            var prev = null;
            var i = middlewares.length;
            // 从最后一个中间件到第一个中间件的顺序开始遍历
            while (i--) {
                // 实际koa的ctx应该指向server的上下文,这里做了简化
                // prev 将前面一个中间件传递给当前中间件
                prev = middlewares[i].call(ctx, prev);
            }
            // while的作用是把第3个生成器(中间件)产生的迭代器作为参数传给第2个生成器,
            // 第2个生成器(中间件)产生的迭代器传给第1个生成器(中间件)
            // 这样第一个中间件执行到yield next(next作为参数已被传入)时,跳转执行第2个生成器(中间
            件)的函数体的部分,以此类推
            // 开始执行第一个中间件
            yield prev;
        });
    }
};

每个中间件都是函数,使用app.use加入到中间件数组,app.listen后开始按顺序执行。

var app = new SimpleKoa();

app.use(function *(next){
    this.body = '1';
    yield next;// yield (function *(next) {...})() 既是迭代器,也是一个生成器函数
    this.body += '5';
    console.log(this.body);
});
app.use(function *(next){
    this.body += '2';
    yield next;
    this.body += '4';
});
app.use(function *(next){
    this.body += '3';
});
app.listen();

如果中间件没有yield语句,则执行完才转下一个中间件,否则直接转下一个中间件,下一个中间件执行完才返回原中间件断点继续执行。

// app.js
const views = require('koa-views')
// views函数(koa-views.ja中的viewsMiddleware函数)执行后得到另一个函数(koa-views.ja中的views函数),此函数被注入到middlewares数组中,等待被执行
app.use(views(path.join(__dirname, 'views')))
// koa-views.js
function viewsMiddleware(
  path,
  { engineSource = consolidate, extension = 'html', options = {}, map } = {}
) {
  return function views(ctx, next) {
    if (ctx.render) return next()

    ctx.response.render = ctx.render = function(relPath, locals = {}) {
       ...
    }

    return next()
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions