ES6系列教程第三篇--Generator 详解

一、什么是Generator 函数

先看下面的Generator函数,

function* helloGenerator() {
console.log("this is generator");
}
这个函数与普通的函数区别是在定义的时候有个*,我们来执行下这个函数。

function* helloGenerator() {
console.log("this is generator");
}
helloGenerator();//没有执行
我们发现,并没有像普通的函数一样,输出打印日志。我们把代码改成下面:

function* helloGenerator() {
console.log("this is generator");
}
var h = helloGenerator();
h.next();
这个时候如期的打印了日志,我们分析下,对于Generator函数,下面的语句

var h = helloGenerator();
仅仅是创建了这个函数的句柄,并没有实际执行,需要进一步调用next(),才能触发方法。

function* helloGenerator() {
yield "hello";
yield "generator";
return;
}
var h = helloGenerator();
console.log(h.next());//{ value: 'hello', done: false }
console.log(h.next());//{ value: 'generator', done: false }
console.log(h.next());//{ value: 'undefined', done: true }
这个例子中我们引入了yield这个关键字,分析下这个执行过程

(1)创建了h对象,指向helloGenerator的句柄,

(2)第一次调用nex(),执行到"yield hello",暂缓执行,并返回了"hello"

(3)第二次调用next(),继续上一次的执行,执行到"yield generator",暂缓执行,并返回了"generator"。

(4)第三次调用next(),直接执行return,并返回done:true,表明结束。

经过上面的分析,yield实际就是暂缓执行的标示,每执行一次next(),相当于指针移动到下一个yield位置。

总结一下,Generator函数是ES6提供的一种异步编程解决方案。通过yield标识位和next()方法调用,实现函数的分段执行。

二、Generator 函数与迭代器(Iterator)

    经过上一篇我们学过迭代器,大家对于迭代器接口的next方法应该不陌生,Generator函数也涉及到next()方法的调用,他们之间有什么关系呢?实现了迭代器接口的对象都可以for-of实现遍历。我们来测试下:

function* helloGenerator() {
yield "hello";
yield "generator";
return;
}
var h = helloGenerator();
for(var value of h){
console.log(value);//"hello","generator"
}
   helloGenerarot对象是支持for-of循环的,也说明Generator函数在原型上实现了迭代器接口,上面调用的next()方法其实就是迭代器的next()方法。我们继续来看next()方法。

function* gen(x,y){
let z= yield x+y;
let result = yield z*x;
return result
}
var g = gen(5,6);
console.log(g.next());//{value: 11, done: false}
console.log(g.next());//{value: NaN, done: false}
分析上面的代码:

1、第一执行next(),运行"yield x+y",并返回x+y的运算结果11;

2、第二次执行next(),运行"yield z*x",此时是z为11,x为5,运算结果为55才对,为何是NaN呢?前一次运行yield x+y的值并没有保存,let z=yield x+y,结果是let z=undefined,所以运算z*x的结果是NaN。

那有没有办法解决这个问题,我们来改下这个例子:

function* gen(x,y){
let z= yield x+y;
let result = yield z*x;
return result
}
var g = gen(5,6);
console.log(g.next());//{value: 11, done: false}
console.log(g.next(11));//{value: 55, done: false}
  请注意,我们第二次调用的时候,next方法中传入了参数11,此时得到正确的结果。next()方法是可以带参数的,其中的参数就替换了上一次yield执行的结果。在这个例子中yield x+y就替换成了11,即

let z=yield x+y 替换成了let z=11,所以得到了正确的值。

道友们可能要问,不能每次都算好上一次的运行结果,作为下一次next的入参吧,这有啥用,我们继续:

function* gen(x,y){
let z= yield x+y;
let result = yield z*x;
return result
}
var g = gen(5,6);
var i =g.next();//{value: 11, done: false}
g.next(i.value);//{value: 55, done: false}
这样就解决了,无论上次运行什么结果,我们都可以作为下一次的值传入。

对于迭代器(Iterator)接口,还有一个return()方法,我们来看下:

function* gen(x,y){
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next();//{value: 1, done: false}
g.next();//{value: 2, done: false}
g.return();//{value: undefined, done: true}
g.next();//{value: undefined, done: true}
执行return()方法后就返回done:true,Generator 函数遍历终止,后面的yield 3不会再执行了。与next()方法一样,return()也可以带参数。

function* gen(x,y){
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next();//{value: 1, done: false}
g.next();//{value: 2, done: false}
g.return(5);//{value: 5, done: true}
g.next();//{value: undefined, done: true}
此时,value就是return传入的值,执行return后结束,调用next(),将不会执行 yield 3。

三、yield 表达式

   上面我们说到yield是Generator函数的暂缓执行的标识,对于yield只能配合Generator函数使用,在普通的函数中使用会报错。可以执行下面的代码,看下结果

function gen(x,y){
yield 1;
yield 2;
yield 3;
}//报错
Generator函数中还有一种yield*这个表达方式,看看它有什么作用。

function* foo(){
yield "a";
yield "b";
}
function* gen(x,y){
yield 1;
yield 2;
yield* foo();
yield 3;
}
var g = gen();
console.log(g.next());//{value: 1, done: false}
console.log(g.next());//{value: 2, done: false}
console.log(g.next());//{value: "a", done: true}
console.log(g.next());//{value: "b", done: true}
console.log(g.next());//{value: "3", done: true}
我们来分析下过程,当执行yield*时,实际是遍历后面的Generator函数,等价于下面的写法:

function* foo(){
yield "a";
yield "b";
}
function* gen(x,y){
yield 1;
yield 2;
for(var value of foo()){
yield value;
}
yield 3;
}
注意:yield* 后面只能适配Generator函数。

四、应用

   讲了这么多,那么Generator函数用在什么场景呢?要回答这个问题,首先我们总结Generator它的特点,一句话:可以随心所欲的交出和恢复函数的执行权,yield交出执行权,next()恢复执行权。我们举几个应用场景的实例。

1、协程

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

  下面我们利用饭店肚包鸡的制作过程来说明,熊大去饭店吃饭,点了只肚包鸡,然后就美滋滋的玩着游戏等着吃鸡。这时后厨就开始忙活了,后厨只有一名大厨,还有若干伙计,由于大厨很忙,无法兼顾整个制作过程,需要伙计协助,于是根据肚包鸡的制作过程做了如下的分工。

肚包鸡的过程:准备工作(宰鸡,洗鸡,刀工等)->炒鸡->炖鸡->上料->上桌

大厨很忙,负责核心的工序:炒鸡,上料

伙计负责没有技术含量,只有工作量的打杂工序:准备工作,炖鸡,上桌

//大厨的活
   function* chef(){
    console.log("fired chicken");//炒鸡
    yield "worker";//交由伙计处理
    console.log("sdd ingredients");//上料
    yield "worker";//交由伙计处理
   }
   //伙计的活
   function* worker(){
       console.log("prepare chicken");//准备工作
       yield "chef";//交由大厨处理
       console.log("stewed chicken");
       yield "chef";//交由大厨处理
       console.log("serve chicken");//上菜
   }
   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);//开始执行
  run(wo);
       }
   }
   run(wo);//开始执行
   我们来分析下代码,我们按照大厨和伙计的角色,分别创建了两个Generator函数,chef和worker。函数中列出了各自角色要干的活,当要转交给其他人任务时,利用yield,暂停执行,并将执行权交出;run方法实现流程控制,根据yield返回的值,决定移交给哪个角色函数。相互配合,直到完成整个过程,熊大终于可以吃上肚包鸡了。

我们执行看下效果,与工序保持一致。

 

2、异步编程

Generator函数,官方给的定义是"Generator函数是ES6提供的一种异步编程解决方案"。我认为它解决异步编程的两大问题

回调地狱
异步流控
回调地狱可以参见我的第一篇Promise,这里不做阐述。

那什么是异步的流控呢,简单说就是按顺序控制异步操作,以上面的肚包鸡为例,每个工序都是可认为异步的过程,工序之间又是同步的控制(上一个工序完成后,才能继续下一个工序),这就是异步流控。

接下来我们从工序的角度,从普通方法实现肚包鸡的制作过程:

setTimeout(function(){
console.log("prepare chicken");
setTimeout(function(){
console.log("fired chicken");
setTimeout(function(){
console.log("stewed chicken");
....
},500)
},500)
},500);
    用setTimeout方法来模拟异步过程,这种层层嵌套就是回调地狱,就是回调地狱,Promise就是解决这种回调的解决方案,有兴趣的可以作为练习,用Promise修改这个例子。

我们用Generator来实现:

//准备
function prepare(sucess){
setTimeout(function(){
console.log("prepare chicken");
sucess();
},500)
}

//炒鸡
function fired(sucess){
setTimeout(function(){
console.log("fired chicken");
sucess();
},500)
}
//炖鸡
function stewed(sucess){
setTimeout(function(){
console.log("stewed chicken");
sucess();
},500)
}
//上料
function sdd(sucess){
setTimeout(function(){
console.log("sdd chicken");
sucess();
},500)
}
//上菜
function serve(sucess){
setTimeout(function(){
console.log("serve chicken");
sucess();
},500)
}

//流程控制
function run(fn){
const gen = fn();
function next() {
const result = gen.next();
if (result.done) return;//结束
// result.value就是yield返回的值,是各个工序的函数
result.value(next);//next作为入参,即本工序成功后,执行下一工序
}
next();
};
//工序
function* task(){
yield prepare;
yield fired;
yield stewed;
yield sdd;
yield serve;
}
run(task);//开始执行
我们来执行下这个过程,按照我们既定的工序顺序实现的。

 

我们分析下执行过程:

1、每个工序对应一个独立的函数,在task中组合成工序列表,执行时将task作为入参传给run方法。run方法实现工序的流程控制。

 2、首次执行next()方法,gen.next()的value,即result.value返回的是prepare函数对象,执行result.value(next),即执行prepare(next);prepre执行完成后,继续调用其入参的next,即下一步工序,

3、以此类推,完成整个工序的实现。

  从上面例子看,task方法将各类工序"扁平化",解决了层层嵌套的回调地狱;run方法,使各个工序同步执行,实现了异步流控。

五、总结

Generator是ES6中非常重要的一个特性,本文仅介绍了其基本的知识和用法,大家有兴趣可以进一步深入了解。
---------------------
作者:恰恰虎
来源:CSDN
原文:https://blog.csdn.net/tcy83/article/details/80427195
版权声明:本文为博主原创文章,转载请附上博文链接!

posted @ 2019-04-09 17:37  鳳舞九天  阅读(126)  评论(0)    收藏  举报