Generator 是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,自动交回执行权。
两种方法可以实现执行器:
在js中实现异步的方式为回调函数,比如读取文件的操作:
fs.readFile(path, function callback(err, data) {
console.log(data);
});
上面的 fs.readFile
有两个参数,第一个路径 path,第二个为回调函数;如果将 readFile
函数改成单参数的函数,类似于函数柯里化,那么结果如下:
function thunkFile(path) {
return function(cb) {
return fs.readFile(path, cb)
}
}
经过转换,变为了单参数的函数,每次只接受一个参数,调用 thunkFile 函数读取文件:
let thunk = thunkFile('./a.txt');
thunk(function(err, data) {
if(err) return;
console.log(data);
});
其中 thunk 就是所谓的 thunk 函数。所谓的 thunk 函数也就是接受一个回调函数为参数的函数。
任何接受回调函数为参数的函数,都可以写成 thunk 函数的形式。下面是一个简单的 Thunk 函数转换器。
// es5版本
function Thunk(fn) {
return function() {
let args = [...arguments];
return function(cb) {
args.push(cb);
return fn.apply(this, args);
};
}
}
generator 要想实现异步操作,可以在 yield 后面接上异步操作的表达式,但是问题是如何才能够保证前一步 yield 执行完了再执行下一步 yield 呢,仅仅自执行肯定是不行的,例如下面的自执行函数
function* gen() {
let result1 = yield readFile('./a.txt');
let result2 = yield readFile(`${result1}.txt`);
console.log(result1);
console.log(result2);
}
var g = gen();
var res = g.next();
while(!res.done){
console.log(res.value);
res = g.next();
}
上面代码中,Generator 函数gen会自动执行完所有步骤。但是,这不适合异步操作。因为 result1 还没有返回,第二个 yield 可能就已经执行了。必须要让 第二个yield 拿到了第一个 yield 的结果之后再执行,这就要求必须在第一个 yield 的回调函数中(因为回调函数中有结果)交回函数的执行权,即执行 g.next()
,这样才能够保证第一个 yield 有了结果之后再执行第二个 yield。
按照这个思路来手动执行上面的 generator 函数:
let g = gen();
let info1 = g.next();
info1.value(function(err, data) {
if(err) throw err;
let info2 = g.next(data);
info2.value(function(err, data) {
if(err) throw err;
g.next(data)
});
})
仔细查看上面的代码,可以发现 Generator 函数的执行过程,其实是将同一个回调函数,反复传入next方法的value属性。这使得我们可以用递归来自动完成这个过程。
到这里,可以自己写一个通用的基于 Thunk 函数的 generator 执行器。
function run(genFn) {
let gen = genFn();
function next(err, data) {
let result = gen.next();
if(result.done) return result.value;
result.value(next);
}
next();
}
上面代码的run函数,就是一个 Generator 函数的自动执行器。内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数的下一步(gen.next
方法),然后判断 Generator 函数是否结束(result.done
属性),如果没结束,就将next函数再传入 Thunk 函数(result.value
属性),否则就直接退出。
有了这个执行器,执行 Generator 函数方便多了。不管内部有多少个异步操作,直接把 Generator 函数传入run函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在yield命令后面的必须是 Thunk 函数。
var g = function* (){
var f1 = yield readFileThunk('fileA');
var f2 = yield readFileThunk('fileB');
// ...
var fn = yield readFileThunk('fileN');
};
run(g);
Thunk 函数并不是实现 generator 函数自执行的唯一办法,因为自执行的关键是,必须有一种机制,自动控制 generator 函数的流程,接收和交换函数的执行权。回调函数可以做到这一点, Promise 也可以。
Promise 实现 generator 执行器的原理是:将异步操作包装成 Promise 对象,用 then 方法中交回执行权
例如文件读取是异步I/O操作,先将读取操作包装成一个promise对象,然后使用then来获取执行权。
let fs = require('fs');
function readFile(path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if(err) return reject(err);
resolve(data);
});
});
}
function* genReadFile() {
let f1 = yield readFile('./a.txt');
let f2 = yield readFile('./b.txt');
}
基于 promise 的 generator 自执行器。
function run(genFn) {
let gen = genFn();
function next(data) {
let result = gen.next(data);
result.value.then(_data => {
next(_data);
});
}
next();
}
// 调用执行器
run(genReadFile);
实现异步 generator 执行器的关键是要确保上一个 yield 返回结果了之后,再继续调用生成器对象的 next() 方法执行下一个 yield。对于异步操作来说,只有在回调函数或者 promise.then
中可以保证当前的异步执行完毕有了结果,所以有了基于 Thunk 函数和promise 对象这两种方式的 generator 执行器。
故,要使用 generator 执行器的话,generator 中的 yield 后面必须接 Thunk 函数或者 promise 对象才行。