Generator函数是ES6新增的异步编程方法,和Promise对象联合使用的话会极大降低异步编程的编写难度和阅读难度,这里参考了多篇优质博文简单总结,欢迎讨论!
一、Generator函数
二、yield语句、return语句
三、next() 可以有参数
四、for...of
五、yield*
六、Generator应用
1. 协程工作
2. 异步编程
Generator与普通函数写法的不同,
1)function*
2)函数内部用 yield 语句定义不同的内部状态,用next方法分段执行不同的内部状态。
3)直接调用Generator函数并不会执行,而是返回一个迭代器对象!
简单的Generator函数及其执行方式如下:
function* g() {
yield 'a';
yield 'b';
return 'ending';
}
var gen = g();
gen.next(); //只有 1行 next(),返回的是 {value:'a', done:false}
gen.next(); //有 2行 next(),返回的是 {value:'b', done:false}
gen.next(); //如果有 3行 next(),返回的是 {value:'ending', done:true}
每调用一次Generator函数,就返回一个迭代器对象,代表Generator函数的内部指针。然后每次调用next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield语句后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。因此,在执行第一个gen.next() 后,返回对象 {value: 'a', done: false},最后执行到return语句后done变为true,表示整个g函数运行完成。
1. yield语句只能用于 function*
的作用域,如果 function*
的内部还定义了其他的普通函数,则函数内部不允许使用 yield 语句。
2. yield语句如果参与运算,必须用括号括起来。
console.log(3 + yield 4); // 语法错误
console.log(3 + (yield 4)); // 打印7
3. 与return的区别
区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。另外,一个函数里面,只能执行一次return语句,但是可以执行多次yield语句。正常函数只能返回一个值,因为只能执行一次return;Generator函数可以返回一系列的值,因为可以有任意多个yield。
4. 运行结果
(1)如果没有return语句:运行最后1个 next() 语句后,返回的仍然是{value: 'b', done: false},需要再调用一次 next() 语句,返回的才是 {value:undefined, done: true},并且这个语句的唯一意义就是证明g函数全部执行完了。
(2)如果return语句后还有yield语句:不会继续执行
(3)如果只有return语句:第一次调用就返回 {value: 'ending', done: true}
(4)如果没有yield和return语句:返回{value:undefined, done: true}
5. Generator返回的迭代器对象也有一个return方法, g.return('value') 会提前终结遍历。
1. next方法的运行逻辑如下:
(1)遇到yield语句,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,也就是不会向左赋值,直到遇到下一个yield语句。
(3)如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
2. 如下所示,对于 var b = yield a++; 由于赋值运算是先计算等号右边,然后赋值给左边,因此该语句运行时,只执行了yield a++,并没有赋值,此时 b 的值仍是undefined。而 next() 中参数的作用就是,为上一个yield语句赋值(因此第一个next()使用参数没有意义),参数一定覆盖的是undefined。
function* g() {
var a = 1;
var b = yield a++;
console.log('b = ' + b); // b = undefined
var c1 = yield b++;
var c2 = yield a++;
}
var gen = g();
console.log(gen.next()); // {value: 1, done: false}
console.log('-------'); // '-------'
console.log(gen.next()); // {value: NaN, done: false}
console.log(gen.next()); // {value: 2, done: false}
注意运行结果中,'-------' 输出顺序在 b = 6之前,说明运行完上一个 next() 后,指针停在该处,即使后面还有普通语句也不再执行,而是等到下一个next() 调用后,Generator函数的指针才开始移动。
3. 下面我们将next() 的参数赋值给了 yield,示例:
function* g() {
var a = 1;
var b = yield a++;
console.log('b = ' + b); // b = 6
var c1 = yield b++;
var c2 = yield a++;
}
var gen = g();
console.log(gen.next());
console.log('-------');
console.log(gen.next(6)); // {value:6, done:false}
console.log(gen.next());
运行结果如下,gen.next() 给yield语句赋了值,使得 b = 6,
4. next参数的实际意义是什么呢?通过next() 的参数可以在Generator函数运行后,往内部注入值,从而调整函数的行为。
for...of 可以遍历迭代器对象,并且不需要调用next 方法,-如下,
function* g() {
yield 1;
yield 2;
yield 3;
return 4;
}
let gen = g();
for(let v of gen){
console.log(v);
}
// 1 2 3
1)当next方法的返回对象的done = true时,for...of 循环就会中止,且不包含该返回对象,因此没有返回4。
2)Generator返回的迭代器对象也有一个return方法, g.return('value') 会提前终结遍历。
如果在Generator函数内部调用另一个Generator函数,需要用 yield* 语句,如下所示:
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'c';
yield* foo(); //yield*调用Generator函数
yield 'd';
}
let ben = bar();
for(let v of ben) {
console.log(v);
}
// c a b d
注意调用Generator函数时,内部的 return 语句是否遍历问题,这里就不作介绍了。
根据Generator的“可以随时交出和恢复函数执行权”的特点,即yield 交出执行权,next 恢复执行权,意味着可以把异步操作写在yield语句里面,等调用 next 方法时再往后执行。
我们有以下的应用场景,
协程指的是多线程间的协作,比如A,B两个线程根据实际逻辑控制共同完成某个任务,A运行一段时间后,暂缓执行,交由B运行,B运行一段时间后,再交回A运行,直到运行任务完成。对于JavaScript单线程来说,我们可以理解为函数间的协作,由多个函数间相互配合完成某个任务。
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 wk = worker();
function run(gen){ //流程控制
var v = gen.next();
if(v.value =="chef"){
run(ch);
}else if(v.value =="worker"){
run(wk);
}
}
run(wk);//开始执行
Generator函数主要用于解决异步编程的两大问题:
异步流控,也就是说按照顺序去控制异步操作,单个任务都是异步任务,多个异步任务之间又是同步的控制。上面的“肚包鸡”流程异步写法如下所示:
function prepare(sucess){
setTimeout(function(){
console.log('准备工作');
sucess();
},500);
}
function fired(sucess){
setTimeout(function(){
console.log('炒鸡');
sucess();
},500);
}
function stewed(sucess){
setTimeout(function(){
console.log('炖鸡');
sucess();
},500);
}
function sdd(sucess){
setTimeout(function(){
console.log('上料');
sucess();
},500);
}
function serve(sucess){
setTimeout(function(){
console.log('上菜');
sucess();
},500);
}
function run(fn){ //流程控制
const gen = fn();
function next() {
const result = gen.next();
if (result.done) return;
result.value(next);
}
next();
};
function* task(){ //工序列表,同步控制-
yield prepare;
yield fired;
yield stewed;
yield sdd;
yield serve;
}
run(task);
虽然Generator函数能够实现异步编程,但实际上我们很少用它来实现异步,因为ES7引入的 async 函数对Generator函数的流程又做了一层封装,使得异步方案使用更加方便。
参考资料:
1. 理解 ES6 Generator 函数
2. ES6系列教程第三篇--Generator 详解
3. 一次搞懂 Generator 函数