以前看过的内容,感觉忘得差不多,最近抽空又看了一次,果然书读百遍其义自见
Generator的执行
Generator函数可以实现函数内外的数据交换和执行权交换。
从第一次调用next
开始,从函数头部开始执行,执行到第一个yield
语句时,把执行权交出到函数外部,并返回该yield
语句右值,同时在此处暂停函数
在下一次调用next
时候(可以传递参数),把执行权返还给函数内部,同时把参数赋值给上一次暂停的yield
语句的左值,并从该行到开始执行到下一个yield
前,并一直循环该过程
需要注意的是,yield
语句的左值,不能由右值赋值,如 let a = yield 3
,a
的值并不等于3,a
的只能由函数外部调用next
时传入的参数赋值。
function test() {
return 3;
}
function* gen(){
console.log(0);
let yield1 = yield 1;
console.log('yield1 value: ', yield1);// yield1: 2
let yield2 = yield test();
console.log('yield2 value: ', yield2);// yield2: 4
return 3;
}
let gen1 = gen();
let next1 = gen1.next();
console.log('next1 value: ', next1);// next: { value: 1, done: false }
let next2 = gen1.next(2);
console.log('next2 value: ', next2);// next: { value: 3, done: false }
let next3 = gen1.next(4);
console.log('next3 value: ', next3);// next: { value: undefined, done: true }
第一次调用
- 从函数顶部开始往下执行,所以首先输出
console.log(0)
- 然后执行
yield1 = yield 1
,此时会把表达式右值返回, 即返回1
- 所以此时
next1 = {value: 1, done: false}
, 接着输出next1
-
gen
函数内部在yield1 = yield 1
处暂停
第二次调用
- 从函数内部
yield1 = yield 1
开始执行 -
注意: 与第一次调用不同,此次调用传入了参数
2
, 第一次调用已经执行了该yield
语句,所以并不会返回右值,而是会进行赋值操作,把传入的参数2
赋给yield1
- 接着执行
console.log('yield1 value: ', yield1)
, 此时yield1 = 2
- 然后执行
yield2 = yield test()
, 此时会把表达式右值返回, 即返回3
- 所以此时
next2 = {value: 3, done: false}
, 接着输出next2
-
gen
函数内部在yield2 = yield test()
处暂停
第三次调用
- 从函数内部
yield2 = yield test()
开始执行 -
注意: 传入了参数
4
, 进行赋值操作,此时yield2 = 4
- 接着执行
console.log('yield2 value: ', yield2)
, 此时的yield2
值为4 - 因为函数内部已经没有
yield
语句,所以一直执行执行到函数尾部return 5
- 所以最后
next3 = {value: 5, done: true}
, 接着输出next2
- 至此函数执行完毕
我们发现Generator函数的执行就是一个循环调用next的过程,自然的想到使用递归来实现自动执行
function* gen() {
let a = yield 1;
let b = yield 2;
let c = yield 3;
}
var g = gen();
var res = g.next();
while(!res.done){
console.log(res.value);
res = g.next();
}
最简单的几行代码,就实现了Generator的"自动执行",但有一个致命的缺点,代码里如果有一步异步操作,并且下一步的操作依赖上一步的结果才能执行,这样的代码就会出错,无法执行,代码如下
function* gen() {
let file1 = yield fs.readFile('a', () => {});
let file2 = yield fs.readFile(file1.name, () => {});
}
var g = gen();
var res = g.next();
// 异步操作,执行file2的yield时
// file1的值为undefined
while(!res.done){
res = g.next(res.value);
}
这就十分尴尬了...使用Generator的一个初衷就是为了避免多层次的回调,写出同步代码
,而我们现在又卡在了回调上,所以需要使用Thunk函数
函数Thunk化
开发中多数情况都不会单独使用Thunk函数,但是把Thunk和Generator结合在一起使用时,就会发生奇妙的化学反应,可以用来实现Generator函数的自动执行。
Thunk化用一句话总结就是,将一个具有多个参数且有包含一个回调函数的函数转换成一个只接受回调函数作为参数的单参数函数,附一段网上的实现
const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
具体原理不多赘述,按照个人理解,函数Thunk化,就是把带有回调函数的函数拆分为两步执行
// 普通函数
function func(a, b, callback){
const sum = a + b;
callback(sum);
}
// 普通调用
func(1, 2, alert);
// 对函数进行Thunk化
const ft = thunkify(func);
// Thunk化函数调用
ft(1, 2)(alert);
包含异步操作的例子,在执行fs.readFile(fileName)
这第一步操作值之后,数据已经拿到,但是不对数据进行操作,而是在第二步的(err, data) => {}
回调函数中进行数据操作
let fs = require('fs');
// 正常版本的readFile
fs.readFile(fileName, (err, data) => {});
// Thunk版本的readFile
fs.readFile(fileName)((err, data) => {});
Generator的自动执行
目前结合Thunk
和Promise
都可以实现
Generator + Thunk
上面报错的例子,把readFile
Thunk化之后,问题就能够得到解决,
let thunkify = require('thunkify');
let readFileThunk = thunkify(fs.readFile);
function* gen() {
let file1 = yield readFileThunk('a');
let file2 = yield readFileThunk(file1.name);
}
var g = gen();
var r1 = g.next();
r1.value(function (err, data) { // 这个回调就是readFileThunk('a')的回调
var r2 = g.next(data); // 等价于file1 = data;
r2.value(function (err, data) {
if (err) throw err;
g.next(data);
});
});
执行next
后返回对象中的value
,不再是一个简单的值,而是一个回调函数,即readFileThunk
的第二步操作,在这个回调函数里,可以取得异步操作的结果,更重要的是可以在这个回调函数中继续调用next
,把函数的执行权返还给gen函数内部,同时把file1的值通过next
的参数传递进去,整个递归就能一直运行。
Generator + Promise
沿用上面的例子,把readFile
包装成一个Promise对象
const readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) return reject(error);
resolve(data);
});
});
};
function* gen() {
let file1 = yield readFileThunk('a');
let file2 = yield readFileThunk(file1.name);
}
var g = gen();
var r1 = g.next();
r1.value.then(function (data) { // 这个回调就是resolve(data)
var r2 = g.next(data); // 等价于file1 = data;
r2.value.then(function ( data) {
if (err) throw err;
g.next(data);
});
});
通过在then
里执行回调函数,获取到上一步操作的结果和交回执行权,并把值传递回gen函数内部,实现了递归执行
进一步封装,可以得到以下的代码
let Bluebird = require('bluebird');
let readFileThunk = Bluebird(fs.readFile);
function run(fn) {
const gen = fn();
function next(err, data) {
const result = gen.next(data);
if (result.done) {
result.value;
} else {
result.value.then((data) => {
next(data);
});
}
}
// 递归执行
next();
}
run(function* g() {
let file1 = yield readFileThunk('a');
let file2 = yield readFileThunk(file1.name);
});
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。