May 2

初探ES6生成器与其妙用

Lrdcq , 2016/05/02 21:31 , 程序 , 閱讀(3498) , Via 本站原創
1.简介

ES6生成器,即generators。生成的是啥?是一个迭代器的生成器,语法参考于c#与python的完全相同的功能,以yield为关键字做为生成器的临时退出点。简单的举举栗子,如果是一个普通的函数,写起来大概是这样的:
function foo(){
  //dosomethings
  return answer;
}
foo();

如果是一个生成器,它看起来会是这样的:
function *foo(){
  //dosomethings
  yield answer1;
  yield answer2;
  return answer3;
}
var ans = foo();
ans.next();//get {value: answer1, done: false}
ans.next();//get {value: answer2, done: false}
ans.next();//get {value: answer3, done: true}

可以看到一些语法特点。首先,定义的时候,前面有一个*号。。。看起来就像指针,当然完全不是,只是原创语法而已。然后是yield关键字。yield是什么意思呢,有退让的意思,这个关键字在c#是yield return,意思就很明确了,这段代码允许到这个地方的时候会暂时退出,并返回一个数据。而这个数据就是当前迭代器迭代出的一个数据。直到代码块运行结束或者return出最后一个数据为止;相对应的,每次在外部调用next的方法,就是让被暂停推出的方法继续运行了。也就是说如果把生成器函数视作一个普通函数,它就在不停的暂停,继续,暂停,继续,直到代码块被运行完成。

当然,既然是普通的代码块运行,当然也可以包含代码流程控制咯。比如如果要写一个1到10的数字迭代器,就可以这样:
function *foo(){
  for(i=1;i<=10;i++){
    yield i;
  }
}

同时,yield关键字还有另一个作用。显然没吃运行都是从yield关键字断开,再从yield关键字回来,那么回来的时候能否携带一点外面的东西么?当然是可以的,参看如下用法:
function *foo(){
  let input = yield 1;
  return input+1;
}
var ans = foo();
ans.next();//get {value: 1, done: false}
ans.next(3);//get {value: 4, done: true}

看清发生了什么么?就是说,每次执行next方法让生成器从中断状态继续运行的时候,可以传入一个数据,这个数据可以做为上一次中断位置的yield的返回值。然后,生成器函数就获得yield返回的值继续进行运算了。

另外,随着迭代生成器的引入,es6还引入了for-of的for循环语法,来辅助对迭代器进行迭代。

2.异步的故事

下面是另一个和生成器完全无关的故事,请听我一一来道。在非常传统的js代码中,异步执行的结果,最后往往是通过callback的方式传递了回来,然后在实际使用中一个回调套一个回调的使用,就像这样:
function act1(callback){
  callback("act1 answer.");
}

function act2(data ,callback){
  callback("act2 answer. = "+data);
}

function main(){
  act1((x)=>{
    act2(x,(y)=>{
      console.log(y);
    });
  });
}

main();

似乎看起来还好,逻辑也很清晰,但是随着业务逻辑复杂,主逻辑中的回调和逻辑代码的层数会越来越深,最终形成js中著名的回调地狱:代码最后一页都是});,这样子。随着代码层次加深,本来清晰的逻辑会变得越来越难以理解,写法越来越难以令人接受。仔细理解这样的业务场景下异步的使用,我们会发现,异步根本没意义嘛,我们只是在等待上一个耗时操作完成,再继续进行下一个操作,整个业务链条其实就是串行的。因此,最清晰的逻辑,我们希望代码是这样的:
function main(){
  var x = act1();
  var y = act2(x);
  console.log(y);
}

当然,这并不可能发生,对于现状我们的act1与act2,在不通过辅助函数的操作,我们根本没法在不传递callback的方法下获得返回值,跟没办法让他们串起来。不过看着生成器功能,有聪明的人想到了这么一个解决方法:
//辅助函数
function warp(foo, ...rest) {
  return function(cb){
    if(rest.length){
      foo(rest,cb);
    }else{
      foo(cb);
    }
  }
}
function run(iter){
   function nextStep(it){
     if (it.done) return;
      if (typeof it.value === 'function'){//Callback
       it.value((ans)=>{
            nextStep(iter.next(ans));
          });
      }else{//Just a value
          nextStep(iter.next(it.value));
      }
    }
    nextStep(iter.next());
}
//主函数
function *main(){
  var x = yield warp(act1);
  var y = yield warp(act2,x);
  console.log(y);
}
run(main());

乍一看没看懂发生了什么吧。那我们慢慢来看。首先第一个辅助函数warp,它的入参是一个函数,和调用这个函数的参数,返回了另一个函数,运行这个函数可以调用入参传入的那个函数,并且可以传入一个callback。也就是说,warp函数的作用是把我们要运行的函数和运行参数准备好,就差一个callback就可以运行了。
然后再看看辅助函数run,它可以传入一个生成器生成的迭代器,里面通过一个递归的方式来处理这个迭代器返回的每一个数据。其中,如果我们遇到了一个函数,也就是warp生成的那种函数,我们会传入一个callback方法,并在它返回数据后对迭代器进行继续运行与递归。
知道发生什么了么。通过warp方法,我们可以讲立刻执行的异步方法作为准备运行的函数吐出迭代器,而迭代器运行函数run可以运行吐出来的每一个方法,并且在方法运行完成之后,把运行结果返回给生成器并让生成器继续运行。这样生成器函数就可以做到在异步操作的时候暂停,然后获得数据再继续运行。看起来就像是异步变同步咯。

当然,warp函数会让代码看起来怪怪的,那么我们来看现在js代码中更常见的情况:Promise。Promise是可以被订阅结果的可以运行函数的包裹,es5中新的网络加载函数fetch就是一个典型的Promise,讲刚才的两个异步方法写成Promise会变成这个样子:
function act1p() {
   return new Promise((resolve, reject)=>{
     resolve("act1 answer.");
   })
};

function act2p(data) {
   return new Promise((resolve, reject)=>{
     resolve("act2 answer. = "+data);
   })
};

function main(){
  act1p().then((x)=>{
    return act2p(x);
  }).then((y)=>{
    console.log(y);
  })
}

main();

现在,在主函数中,已经不存在回调地狱了,不过整个代码的流程还是看起来怪怪的,也并不整齐,那么我们能用类似的方法用生成器函数来解决么?当然,这次,我们不需要warp了,代码看起来是这样的:
function run(iter){
   function nextStep(it){
     if (it.done) return;
      if (typeof it.value === 'function'){//Callback
       it.value((ans)=>{
            nextStep(iter.next(ans));
          });
      }else if (typeof it.value.then === 'function'){//Promise
       it.value.then((ans)=>{
         nextStep(iter.next(ans));
       });
      }else{//Just a value
          nextStep(iter.next(it.value));
      }
    }
    nextStep(iter.next());
}

function *main(){
  var x = yield act1p();
  var y = yield act2p(x);
  console.log(y);
}
run(main());

我们在辅助函数run中新增了处理Promise的方法,如果我们遇到的是一个Promise,则去then它来获得我们需要的结果。由于Promise方法本身就是返回的Promise,那么主函数中也就不需要warp,直接运行Promise方法,就像普通使用它们一样的写法就可以了。看起来不是更贴近异步变同步了么?

这个方法,如果更广泛的支持更多的异步方法执行格式,我们就得到了最近nodejs上火成渣渣的第三方库:co(https://github.com/tj/co),就这么百来行代码,在最近nodejs开发过程中可算是帮大忙。

3.未来的写法

有了ES6的生成器和co库,我们是否就能够掌握未来了?不,ES7推出了async/await套装来解决这个问题。毕竟用迭代生成器来解决异步问题从语意上说不通嘛。js是一门开放的语言,可以随心所欲的灵活使用各种特性,也会快速吸纳社区中各种优秀与有必要的特性。async/await特性算是官方将生成器与co库融合在了一起,将上面的main生成器用async/await改写一下,会是这样的:
async function main(){
  var x = await act1p();
  var y = await act2p(x);
  console.log(y);
}
main();

语意自然,流程,一看就可以看出我们做了什么,这才是来自未来的解决方案。
logo