基本概念
可以将Generator
理解成状态机,里面封装了多个内部状态。
function* testGenerator(){
yield '开始';
yield '暂停';
yield '继续';
yield '结束';
}
var test = testGenerator();
console.log(test.next()); // { value: '开始', done: false }
console.log(test.next()); // { value: '暂停', done: false }
console.log(test.next()); // { value: '继续', done: false }
console.log(test.next()); // { value: '结束', done: false }
console.log(test.next()); // { value: undefined, done: true }
Generator
跟普通函数相比,在声明时候多携带了*
,其次内部使用了yield
。
当调用testGenerator
时,并不会执行函数内部的功能,而是返回一个指向内部状态的指针。只有当调用指针的next()
时,就会执行函数内部的功能,指导遇到yield
或return
为止。下一次的next
则会从上一次停止的yield
方法继续往下执行,直到遇到下一个yield
为止。
故调用testGenerator()
返回了一个状态指针,并保存在了test
变量中。第一次调用next
时,执行到了yield '开始'
,则输出了开始
并结束。第二次调用yield
时,从上一次结束的位置继续往下,遇到了yield '暂停'
,输出暂停
并结束。依次类推。直到最后一次next()
被调用时,由于内部已经没有yield
方法,所以value: undefined, done: true
。
与Iterator的关系
任意一个对象的
Symbol.iterator
方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator
属性,从而使得该对象具有 Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
console.log(...myIterable) // 1 2 3
for(key of myIterable){
console.log(key); // 1 2 3
}
next
yield
本身是没有返回值的,但是我们可以通过next
给yield
传递返回值。next
方法携带的参数会被当作上一个yield
表达式的返回值。
function* test() {
for(let i = 0; true; i++){
var reset = yield i;
console.log(`第${i}次reset=${reset}`);
}
};
var t = test();
t.next(1);
t.next(2); // 第0次reset=2
t.next(3); // 第1次reset=3
- 执行
t.next(1)
时,执行到了yield 0
,结束; - 执行
t.next(2)
时,由于它存在上一个yield
表达式,则此时reset=2
,执行yield 2
结束; - 执行
t.next(3)
时,由于它存在上一个yield
表达式,则此时reset=3
,执行yield 3
结束。
注意,由于
next
方法的参数表示上一个yield
表达式的返回值,所以在第一次使用next
方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next
方法时的参数,只有从第二次使用next
方法开始,参数才是有效的。从语义上讲,第一个next
方法用来启动遍历器对象,所以不用带有参数。
参考ES6
书上的例子:
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
// =================错误的做法==============
var a = foo(5);
// 此时执行 yield(5+1), 所以输出{value: 6, done: false}
console.log(a.next());
// y依靠的是上一次的yield的返回值,由于这次没有给next传参,所以上一次yield(x+1)的返回值是undefined
// y = undefined, 则执行yield(undefined/3),则为{value: NaN, done: false}
console.log(a.next());
// z依靠的是上一次的yield的返回值,由于这次没有给next传参,所以上一次yield (y / 3)的返回值是undefined
// z = undefined, 执行5+undefined+undefined = NaN, 则为{value: NaN, done: false}
console.log(a.next());
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
// =================正确的做法==============
var b = foo(5);
// 此时执行 yield(5+1), 所以输出{value: 6, done: false}
var res1 = b.next();
console.log(res1.value); // 6
// y依靠的是上一次的yield的返回值,由于这次给next传参6,所以上一次yield(x+1)的返回值是6
// y = 12, 则执行yield(12/3),则为{value: 4, done: false}
var res2 = b.next(res1.value);
console.log(res2.value); // 4
// z依靠的是上一次的yield的返回值,由于这次给next传参4,所以上一次yield (y / 3)的返回值是4
// z = 4, 执行5+12+4 = NaN, 则为{value: 21, done: false}
var res3 = b.next(res2.value);
console.log(res3.value); // 21
实际应用
协程
猪肚鸡的流程是:准备工作、炒鸡、炖鸡、上料、上桌,其中:
- 主厨负责: 炒鸡、上料;
- 伙计负责: 准备工作、炖鸡、上桌。
function* chef(){
console.log('炒鸡');
yield "worker";
console.log('上料');
yield "worker";
}
function* worker(){
console.log('准备工作');
yield "chef";
console.log('炖鸡');
yield "chef";
console.log('上桌');
}
var ch = chef();
var wo = worker();
function run(gen){
var v = gen.next();
if(v.value === 'chef'){
run(ch);
}else if(v.value =="worker"){
run(wo);
}
}
run(wo);
异步编程同步化
function fetchData(url, cb){
return function(cb){
setTimeout(function(){
cb({status: 200, data: url})
}, 1000)
}
}
function* gen(){
var r1 = yield fetchData('https://api.github.com/users/github');
var r2 = yield fetchData('https://api.github.com/users/github/followers');
console.log([r1.data, r2.data].join('\n'));
}
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if(result.done) return;
// 这里的value就是fetchData返回的回调函数,所以会执行next
result.value(next);
}
next();
}
run(gen);
在这里封装了run
函数,可以将run
理解成是一个执行器,自动执行Generator
函数。Generator
+执行器就可以实现async/await
。这个后面学习async/await
会记录一下。