ES6基础——Generator函数详解

Generator函数是ES6新增的异步编程方法,和Promise对象联合使用的话会极大降低异步编程的编写难度和阅读难度,这里参考了多篇优质博文简单总结,欢迎讨论!

一、Generator函数

二、yield语句、return语句

三、next() 可以有参数

四、for...of

五、yield*

六、Generator应用

1. 协程工作

2. 异步编程


一、Generator函数

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函数运行完成。

 

二、yield语句、return语句

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') 会提前终结遍历

 

三、next() 可以有参数

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}

                                              ES6基础——Generator函数详解_第1张图片

注意运行结果中,'-------' 输出顺序在 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,

                              ES6基础——Generator函数详解_第2张图片

4. next参数的实际意义是什么呢?通过next() 的参数可以在Generator函数运行后,往内部注入值,从而调整函数的行为

 

四、for...of

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') 会提前终结遍历

 

五、yield*

如果在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应用

根据Generator的“可以随时交出和恢复函数执行权”的特点,即yield 交出执行权,next 恢复执行权,意味着可以把异步操作写在yield语句里面,等调用 next 方法时再往后执行

我们有以下的应用场景,

1. 协程工作

协程指的是多线程间的协作,比如A,B两个线程根据实际逻辑控制共同完成某个任务,A运行一段时间后,暂缓执行,交由B运行,B运行一段时间后,再交回A运行,直到运行任务完成。对于JavaScript单线程来说,我们可以理解为函数间的协作,由多个函数间相互配合完成某个任务。

ES6基础——Generator函数详解_第3张图片

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);//开始执行

 

2. 异步编程

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 函数

你可能感兴趣的:(JavaScript基础,javascript)