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()
}
}
koa 中间件机制实现原理
一个简单的co实现
调用
生成器(*)函数调用只会返回迭代器,不会执行函数体,只有迭代器.next()才会从头开始按步骤执行生成器函数体。所以fn.call(ctx)并不会打印c
第一次调用 _next() ,执行到
gen.next(res),执行完read('error.js')后,执行 it 的赋值是然后执行
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
这样就不能把读取的文件数据传给fs.readFile函数外的变量a,因为只有
fs.readFile(filename, (err,result) => {})的回调函数参数才能读取其参数result,所以想要得到result,就要回调函数把result从自身函数体内传出来。怎么传出来呢?你可能想定义一个全局变量,然后在回调函数体内把结果result赋值给它。这样不行,因为主线程不知道异步线程什么时候完成读取文件工作并把result赋值给这个全局变量。这样造成你的全局变量在主线程使用时可能是undefined
答案是用在回调函数内
迭代器.next(result),等我做好所有工作了再传出来例如,没用Thunk
这时需要一个任务执行器,
但是a和b并没有接收到数据
这也是Thunk的作用体现出来了,任务执行器根据obj的value是否为函数(Thunk)来判断执行回调函数(回调函数决定什么时候执行下一个yield)还是直接执行下一个yield
实现简单的 koa 框架(剥离除中间件外所有的逻辑)
每个中间件都是函数,使用app.use加入到中间件数组,app.listen后开始按顺序执行。
如果中间件没有yield语句,则执行完才转下一个中间件,否则直接转下一个中间件,下一个中间件执行完才返回原中间件断点继续执行。