搞定 koa 之 Nodejs co源码
搞定 koa 之 Nodejs co源码
书接上文,这次我们来详细看看 co 的源码,这是了解 koa 的必要步骤。
系列目录
在看源码前,我们再看段 generator 的代码
function* Gen(a){
var b = yield a;
console.log(b);//3
var c = yield b;
yield c;
}
var g = Gen(1);
console.log(g.next(2));//{ value: 1, done: false }
console.log(g.next(3));//{ value: 3, done: false }
console.log(g.next(4));//{ value: 4, done: false }
console.log(g.next());//{ value: undefined, done: true }
- 有点奇怪的结果,传入的2没有被返回
- 调用 Gen 的时候,会为 a 初始化一个值,所以 g.next(2)返回1
- 调用 g.next(3)的时候会为 b 赋值3,也就是说调用 g.next(2)的时候并没有为 b 赋值,只有再次调用 next,函数才会继续运行,执行到 b 赋值的部分。
看懂了上面的代码,再看看 co 是如何用的
var co = require('..');
var fs = require('fs');
function read(file) {
return function(fn){
fs.readFile(file, ‘utf8’, fn);
}
}
co(function *(){
var a = yield read(’.gitignore’);
var b = yield read(‘Makefile’);
var c = yield read(‘package.json’);
console.log(a.length);
console.log(b.length);
console.log(c.length);
});
- co 需要传入一个 generatorFunction
- co 的原理很简单,就是利用 next 逐步执行以及可以给 next 传参数来为变量赋值
- 例如上面的代码,开始co执行generatorFunction,然后调用next,就可以获得 read(’.gitignore’)的返回值。这里的 read 会返回一个函数。
- co 会往这个函数里面传入一个回调函数,readFile 完成时,回调函数会被触发,这个回调函数的逻辑就是调用 next(data)。这样 a 变量就会获得 readFile 返回的结果了。
看懂上面的代码,我们正式进入 co 的源码
function co(fn) {
//判断是否为 generatorFunction
var isGenFun = isGeneratorFunction(fn);
return function (done) {
var ctx = this;
// in toThunk() below we invoke co()
// with a generator, so optimize for
// this case
var gen = fn;
// we only need to parse the arguments
// if gen is a generator function.
if (isGenFun) {
//把 arguments 转换成数组
var args = slice.call(arguments), len = args.length;
//根据最后一个参数是否为函数,判断是否存在回掉函数
var hasCallback = len && 'function' == typeof args[len - 1];
done = hasCallback ? args.pop() : error;
//执行 generatorFunction
gen = fn.apply(this, args);
} else {
done = done || error;
}
//调用 next 函数,这是一个递归函数
next();
// #92
// wrap the callback in a setImmediate
// so that any of its errors aren't caught by `co`
function exit(err, res) {
setImmediate(function(){
done.call(ctx, err, res);
});
}
function next(err, res) {
var ret;
// multiple args
if (arguments.length > 2) res = slice.call(arguments, 1);
// error
if (err) {
try {
ret = gen.throw(err);
} catch (e) {
return exit(e);
}
}
// ok
if (!err) {
try {
//执行 next,会获得 yield 返回的对象。同时通过 next 传入数据,为变量赋值
//返回的对象格式是{value:xxx,done:xxx},这里的 value 是一个函数
ret = gen.next(res);
} catch (e) {
return exit(e);
}
}
// done 判断是否完成
if (ret.done) return exit(null, ret.value);
// normalize
ret.value = toThunk(ret.value, ctx);
// run
if ('function' == typeof ret.value) {
var called = false;
try {
//执行 ret.value 函数,同时传入一个回调函数。当异步函数执行完,会递归 next
//next又会执行gen.next(),同时把结果传出去
ret.value.call(ctx, function(){
if (called) return;
called = true;
next.apply(ctx, arguments);
});
} catch (e) {
setImmediate(function(){
if (called) return;
called = true;
next(e);
});
}
return;
}
// invalid
next(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following was passed: "' + String(ret.value) + '"'));
}
}
}
参考资料
联系我
搞定 koa 之 Nodejs co源码
书接上文,这次我们来详细看看 co
的源码,这是了解 Koa 的必要步骤。
系列目录
在看源码前,我们再看段 generator 的代码:
function* Gen(a) {
var b = yield a;
console.log(b); // 输出 3
var c = yield b;
yield c;
}
var g = Gen(1);
console.log(g.next(2)); // { value: 1, done: false }
console.log(g.next(3)); // { value: 3, done: false }
console.log(g.next(4)); // { value: 4, done: false }
console.log(g.next()); // { value: undefined, done: true }
- 奇怪的结果:传入的
2
没有被返回。 - 调用 Gen 的时候:会为
a
初始化一个值,所以g.next(2)
返回1
。 - 调用
g.next(3)
:会为b
赋值3
,也就是说调用g.next(2)
的时候并没有为b
赋值,只有再次调用next
,函数才会继续运行,执行到b
赋值的部分。
看懂了上面的代码,再看看 co
是如何用的:
var co = require('..');
var fs = require('fs');
function read(file) {
return function(fn) {
fs.readFile(file, 'utf8', fn);
}
}
co(function* () {
var a = yield read('.gitignore');
var b = yield read('Makefile');
var c = yield read('package.json');
console.log(a.length);
console.log(b.length);
console.log(c.length);
});
- co 需要传入一个 generatorFunction。
- co 的原理:利用
next
逐步执行以及可以给next
传参数来为变量赋值。 - 例如上面的代码:开始
co
执行generatorFunction
,然后调用next
,就可以获得read('.gitignore')
的返回值。这里的read
会返回一个函数。 - co 会往这个函数里面传入一个回调函数:
readFile
完成时,回调函数会被触发,这个回调函数的逻辑就是调用next(data)
。这样a
变量就会获得readFile
返回的结果了。
看懂上面的代码,我们正式进入 co
的源码:
function co(fn) {
var isGenFun = isGeneratorFunction(fn);
return function (done) {
var ctx = this;
var gen = fn;
if (isGenFun) {
var args = Array.prototype.slice.call(arguments);
var hasCallback = args.length && 'function' == typeof args[args.length - 1];
done = hasCallback ? args.pop() : error;
gen = fn.apply(this, args);
} else {
done = done || error;
}
next();
function exit(err, res) {
setImmediate(function(){
done.call(ctx, err, res);
});
}
function next(err, res) {
var ret;
if (arguments.length > 2) res = Array.prototype.slice.call(arguments, 1);
if (err) {
try {
ret = gen.throw(err);
} catch (e) {
return exit(e);
}
}
if (!err) {
try {
ret = gen.next(res);
} catch (e) {
return exit(e);
}
}
if (ret.done) return exit(null, ret.value);
ret.value = toThunk(ret.value, ctx);
if ('function' == typeof ret.value) {
var called = false;
try {
ret.value.call(ctx, function(){
if (called) return;
called = true;
next.apply(ctx, arguments);
});
} catch (e) {
setImmediate(function(){
if (called) return;
called = true;
next(e);
});
}
return;
}
next(new TypeError('You may only yield a function, promise, generator, array, or object, but the following was passed: "' + String(ret.value) + '"'));
}
}
}
- 判断是否为 generatorFunction:通过
isGeneratorFunction
方法。 - 生成器函数的处理:如果
fn
是一个生成器函数,则将arguments
转换成数组,并检查最后一个参数是否为回调函数。 - 执行生成器函数:使用
fn.apply(this, args)
来执行生成器函数。 - 递归调用
next
函数:通过next()
来逐步执行生成器函数。 - 处理异步操作:如果
ret.value
是一个函数,则调用该函数并传入回调函数,当异步操作完成时,回调函数会触发next
函数,从而继续执行生成器函数。
希望这些解释和代码示例能帮助你更好地理解 co
的工作原理。
说2点markdown用法吧
- 标题里的## 后面要加一个空格,这是因为cnodejs用的markdown编译器不支持不带空格的写法
- 不要从2级标题【参考资料】跳到4级标题【联系我】,标题要有连续性
http://purplebamboo.github.io/2014/05/24/koa-source-analytics-1/ 我也写过一个系列,跟楼主一样。。按照 generator co koa的顺序 挨个的介绍。。
受教了
在深入理解 co
源码之前,我们需要先回顾一下 co
的基本用途。co
的核心功能是将 Generator 函数转换为同步的流程,从而简化异步操作。接下来我们将逐步解析 co
源码。
源码解析
首先,定义 co
函数:
function co(fn) {
// 判断是否为 Generator 函数
var isGenFun = isGeneratorFunction(fn);
return function (done) {
var ctx = this;
var gen = fn;
// 如果是 Generator 函数,则处理参数
if (isGenFun) {
var args = Array.prototype.slice.call(arguments, 0, -1);
done = arguments[arguments.length - 1];
gen = fn.apply(this, args);
} else {
done = done || error;
}
// 开始递归调用 next
next();
// 处理错误退出
function exit(err, res) {
setImmediate(function(){
done.call(ctx, err, res);
});
}
// next 函数递归调用
function next(err, res) {
var ret;
if (err) {
try {
ret = gen.throw(err);
} catch (e) {
return exit(e);
}
} else {
try {
ret = gen.next(res);
} catch (e) {
return exit(e);
}
}
if (ret.done) return exit(null, ret.value);
// 将 yield 的值转为 Thunk 形式
ret.value = toThunk(ret.value, ctx);
if ('function' == typeof ret.value) {
var called = false;
try {
ret.value.call(ctx, function(){
if (called) return;
called = true;
next.apply(ctx, arguments);
});
} catch (e) {
setImmediate(function(){
if (called) return;
called = true;
next(e);
});
}
return;
}
next(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following was passed: "' + String(ret.value) + '"'));
}
};
}
// 假设 isGeneratorFunction 和 toThunk 已经实现
示例代码
我们来看一个具体的使用案例:
const co = require('co');
function read(file) {
return function(callback) {
fs.readFile(file, 'utf8', callback);
};
}
co(function *() {
const a = yield read('.gitignore');
const b = yield read('Makefile');
const c = yield read('package.json');
console.log(a.length);
console.log(b.length);
console.log(c.length);
});
解释
- Generator 函数:在
co
中,yield
后面的表达式必须是可以被next
调用的对象,如函数、Promise、Generator、数组或对象。 - co 函数:
co
接收一个 Generator 函数,并返回一个接受done
回调的函数。co
通过递归调用next
函数,逐步执行 Generator 函数中的每一个yield
表达式。 - 错误处理:在每次调用
gen.next()
或gen.throw()
时,都会捕获可能的异常并调用exit
函数进行错误处理。 - 异步处理:对于
yield
返回的函数,co
会将其包装成一个回调函数并立即调用,从而确保异步操作的正确执行。
通过以上代码和解析,你可以更好地理解 co
如何将异步操作串行化并简化代码编写。