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,就将结果返回,实际没有执行z的赋值;第二次运行时,执行的是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执行的结果。在这个例子中z复制为11,即执行了let z=11,所以运行z*x时,返回了正确的值。

道友们可能要问,不能每次都算好上一次的运行结果,作为下一次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}

这样就解决了,next()方法执行后,会返回yield后面表达式的运算结果,将上一次的运算结果,作为下一次的入参传入,实现了"无缝对接"。

这里我们用这个例子主要解释next的入参和返回值的用法,在实际工程中,我们可以用下面这种简单的方式(特别感谢恍惚丶旧时光同学的提醒),大家可以琢磨下。

function* gen(x,y){
   let z,result;
   yield z= x+y;
   yield result = z*x;
   return result
}
var g = gen(5,6);
g.next();//{value: 11, done: false}
g.next();//{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);//开始执行

    我们来分析下代码,我们按照大厨和伙计的角色,分别创建了两个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() {
        //返回工序函数的句柄给result
        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);//开始执行

我们来执行下这个过程,按照我们既定的工序顺序,每隔500ms打印如下内容。

我们分析下执行过程:

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

 2、首次执行next()方法,进行next方法体内。

const result = gen.next();gen为传入的task函数,执行yield prepare,并将prepare函数对象句柄返回给result(注意:此时并没有执行prepare函数);

if (result.done) return;返回是否是结束状态,由于是第一步,为false跳过。

result.value(next);result.value即是prepare函数对象,next方法作为入参传入。执行result.value(next),其实就是执行prepare(next);prepre执行完成后,继续调用其入参的next(success())方法,即下一步工序,

3、以此类推,直到最后一步完成后,result.done为ture,结束整个工序。

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

五、总结

Generator是ES6中非常重要的一个特性,本文仅介绍了其基本的知识和用法,大家有兴趣可以进一步深入了解。

上一篇:ES6系列教程第二篇--Iterator 详解                                                               下一篇:ES6系列教程第四篇--asyn详解

  • 29
    点赞
  • 66
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值