Appearance
co 的实现
co 的实现非常精巧,严重推荐阅读 co 的源码。
co 代码优化过,新的4.x.x的代码基于Promise重构过,比以前的代码好理解很多。
上一个版本的可以看3.1.0的代码,很晦涩,不好理解。
4.x.x 的 api 有些变化,co 返回的是一个 promise,这部分的内容留到最后一节讲解。
一个最简单的 co 实现:
javascript
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();
}
}
调用:
javascript
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);
})();
请自行复制以上代码,并给觉得有疑惑的地方打上断点,查看逻辑的执行流程。
我们需要解开如下几个关键问题:
- 问题1:为什么异步函数需要封装成 thunk 偏函数的形式?
- 问题2:var a = yield read('error.js'); 为什么 a 的值是 read() 异步返回的数据?
- 问题3:co 接受的 generator function 内部执行逻辑与co内部逻辑的执行顺序是什么样的?
(PS: yield 后的内容称之为 yieldable)
var gen = fn.call(ctx); fn 是co的实参,为一个generator function,再次强调 generator function 调用时,只定义不执行,与普通函数不同。
co 的核心是想办法递归调用 generator function 的 next(),直到执行完毕,_next() 就是个这样的递归方法:
javascript
function _next(err, res) {
if(err) res = err;
it = gen.next(res);
//{value:function(){},done:false}
if(!it.done){
it.value(_next);
}
}
_next();
第一次调用 _next() ,执行到 gen.next(res); 开始执行 generator function 遍历器的逻辑:
javascript
var c = 2;
console.log(c);
yield read('error.js');
留意不是执行到 var a = 给a赋值(赋值是在 yield 逻辑之后)的逻辑,遍历器执行到 yield 关键字就中断了,开始执行 read 这个 thunk 的逻辑。
read是一个偏函数,只做一件事情,返回一个函数:
javascript
function(fn){
fs.readFile(file, 'utf8', fn);
}
核心问题:这里的 fn 的实参是什么?
执行完 read() 后,执行 it = (co中的 it = gen.next(res); )的赋值,it的值是:
javascript
{value:function(fn){
fs.readFile(file, 'utf8', fn);
},done:false}
(参见第一节 generator)value 即偏函数 return 的函数,因为遍历器知道后面还有 yield 标识,也就值没有执行完,所以 done 为 false。
接下来我们执行 value 函数:
javascript
if(!it.done){
it.value(_next);
}
解答前面的核心问题,function(fn){fs.readFile(file, 'utf8', fn);} fn 指的是 _next 遍历方法。
正式执行异步逻辑获取文件数据:fs.readFile(file, 'utf8', fn); 且执行完后,执行 _next()。
所以 _next 带有二个形参:err, res,这是 fs.readFile(err,res) 的回调函数的形参。
res 就是读取文件后返回的文件内容。
这就解答了前面的 第一个问题,为什么异步函数需要封装成 thunk 偏函数的形式,因为我们需要在异步执行完成后,触发 _next 遍历方法。
_next 进入第二次调用,又执行了次 gen.next(res) 将 res 塞回到遍历器内部,且遍历器执行第二段逻辑:
javascript
var a = res;
console.log(a.length);
yield read('package.json');
这就解答了前面的 第二个问题,var a = yield read('error.js'); 为什么 a 的值是 read() 异步返回的数据?那是因为 a 的值是 gen.next() 塞回去的。
执行到 yield 关键字时,又中断了,执行 read 偏函数,it.value(_next) 执行偏函数的返回函数,异步读取文件内容。
读取完文件后,又再次触发了 _next() 方法 直到 done 为 true 时。
这就解答了 第三个问题:co 接受的 generator function 内部执行逻辑与co内部逻辑的执行顺序是什么样的?
当你搞清楚了这三个问题,就清楚了 co 的实现原理。