美文网首页
36_谈谈你对es6中的Generator函数的认识

36_谈谈你对es6中的Generator函数的认识

作者: 沐向 | 来源:发表于2020-04-10 09:52 被阅读0次

一、generator是什么

generator是ES6提供的一种异步编程解决方案,在语法上,可以把它理解为一个状态机,内部封装了多种状态。执行generator,会生成返回一个遍历器对象。返回的遍历器对象,可以依次遍历generator函数的每一个状态。同时ES6规定这个遍历器是Generator函数的实例,也继承了Genarator函数的prototype对象上的方法。

最简单的generator函数,其实它就是一个普通的函数,但是它有两个特征。

  • 第一就是function关键字与函数名之间有一个*号
  • 其二就是函数体内使用yield表达式来遍历状态
function* newGenerator(){
    yield 'hello';
    yield 'world';
    return 'ending';
}

执行generator函数之后,该函数并不会立即执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。通常使用遍历器对象的next方法。使得指针移向下一个状态。每一次调用next()方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式位置,由此可以看出,generator是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

注:generator函数可以随心所欲的交出和恢复函数的执行权,yield交出执行权,next()恢复执行权

二、generator中的yield表达式

yield表达式在generator中是作为一个暂停标志,当碰到yield时,函数暂停执行,等到下一次next()执行时,函数才从当前yield位置开始执行。并且,yield表达式只能用在Generator函数里边;同时,yield如果后边带一个,则就是相当于一个for...of的简写形式,如果yield后边不带,则返回的是generator的值。

function* gen() {
    yield 'hello';
    yield* 'hello';
}
let f = gen();
console.log(f.next().value);
console.log(f.next().value);
console.log(f.next().value);
console.log(f.next().value);
console.log(f.next().value);

上述例子中的后四个next()函数,就会顺序的返回h e l l

三、generator中的next函数

通过next函数,可以执行对应的yield表达式,且next()函数还可以带参数,该参数可以作为上一次yield表达式的返回值,因为yield本身是没有返回值的,如果next()中不带参数,则yield每次运行之后的返回值都是为undefined;

function* dataConsumer() {
    console.log('Started');
    console.log(`1. ${yield}`);
    console.log(`2. ${yield}`);
    return 'result';
}

let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a');
// 1. a
genObj.next('b');
// 2. b

上述函数中,第一次运行next(),运行到第一个next()函数截止,第二个next运行时,传入的参数为'a';则运行到第二个yield地方截止,然后第一个yield运行的返回值为'a',依次类推,则得到上述结果。

另外,通过for...of可以循环generator中的所有状态,并且不需要使用next()函数。除了for...of循环以外,扩展运算符(...),解构赋值和Array.form方法内部调用的,都是遍历器接口。

generator生成的对象,还有其他一些函数,比如throw()用来抛出错误,return()用来定义返回值并终止generator的状态。

以上的三个方法在本质上其实是一样的,他们就是让generator恢复执行,并且使用不同的语句来替代yield语句。

  • next()是将yield表达式替换成一个值
  • throw()是将yield表达式替换成一个throw语句
  • return()是将yield表达式替换成一个return语句

四、Generator与协程

协程可以理解为“协作的线程”或者“协作的函数”。协程既可以是单线程实现,也可以用多线程实现,前者是一种特殊的子例程,后者是一种特殊的线程。

协程有点像函数,又有点像线程,它的运行流程大致如下。

  • 第一步,协程A开始执行
  • 第二部,协程A执行到一半,进入暂停,执行权转移到协程B
  • 第三步,(过了一段时间)协程B交换执行权
  • 最后,协程A恢复执行

协程适合用于多任务运行环境,它与普通的线程很相似,都有自己的执行上下文,可以分享全局量。他们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都是处于暂停状态。

由于JavaScript是单线程,只能保持一个调用栈,引入协程之后,每一个任务可以保持自己的调用栈,这样就可以再抛出错误的时候找到原始的调用栈,不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束了。

Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。

如果将Generator函数当做协程,完全可以将多个需要互相协作的任务写成Generator函数,他们之间使用yield标识交换控制权。

Generator 函数执行产生的上下文环境,一旦遇到yield命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。

五、Generator函数的应用

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提供的一种异步编程解决方案"。我认为它解决异步编程的两大问题

  • 回调地狱
  • 异步流控

那什么是异步的流控呢,简单说就是按顺序控制异步操作。下面我们就从工序的角度重新实现肚包鸡实例(上面的肚包鸡制作实例,是从角色的角度,这种模式其实并不是最佳的),每个工序都是可认为异步的过程,工序之间又是同步的控制(上一个工序完成后,才能继续下一个工序),这就是异步流控。

普通方法实现肚包鸡的制作过程:

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打印如下内容。

我们分析下执行过程:

  • 每个工序对应一个独立的函数,在task中组合成工序列表,执行时将task作为入参传给run方法。run方法实现工序的流程控制。
  • 首次执行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函数的面试题

1、解决AJAX的异步回调嵌套

ajax('http://url-1', data1, function (err, result) {
    if (err) {
        return handle(err);
    }
    ajax('http://url-2', data2, function (err, result) {
        if (err) {
            return handle(err);
        }
        ajax('http://url-3', data3, function (err, result) {
            if (err) {
                return handle(err);
            }
            return success(result);
        });
    });
});

将以上代码用generator函数改成同步执行方式:

try {
    r1 = yield ajax('http://url-1', data1);
    r2 = yield ajax('http://url-2', data2);
    r3 = yield ajax('http://url-3', data3);
    success(r3);
}
catch (err) {
    handle(err);
}

2、用generator函数生成一个自增的ID

var current_id = 0;

function next_id() {
    current_id ++;
    return current_id;
}

将以上代码用generator函数进行实现:

function* next_id() {
    for (id = 1; ; id++) {
        yield id;
    }
}

七、参考资料

相关文章

网友评论

      本文标题:36_谈谈你对es6中的Generator函数的认识

      本文链接:https://www.haomeiwen.com/subject/udylmhtx.html