Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES6 generator 、yield 与co #49

Open
youngwind opened this issue Mar 22, 2016 · 4 comments
Open

ES6 generator 、yield 与co #49

youngwind opened this issue Mar 22, 2016 · 4 comments
Labels

Comments

@youngwind
Copy link
Owner

youngwind commented Mar 22, 2016

起因

最近在看一些node项目的时候发现里面用到了ES6的generator函数,yield和tj的co库,花了一些时间搞明白它们之间的关系,下面用一些例子说明。

溯源

对于异步的操作,最常规的写法是回调函数,但是深度回调会出现可怕的金字塔。那么,如何用更好的书写方式来避免金字塔,又或者说,怎么样把异步的代码写得看起来好像同步那样子呢?
其中一种解决方案是promise模式,.then一直then下去。ok,从ES6开始,有两个新的特性,叫generator和yield,借助它们,我们能够更优雅地解决这个问题。

generator和yield简介

请看下面的代码

function* Hello(){
 yield 1;
 yield 2;
}
var hello = Hello();
console.log(hello.next());  // { value:1, done:false }
console.log(hello.next());  // {  value:2, done:false }
console.log(hello.next());  // { value:undefined, done:true }
  1. function后面的*号代表这是一个generator函数,而非普通函数,只有在generator函数中才能使用yield,在普通函数中使用yield会报错。
  2. generator函数的函数是分段的。第一次执行next的时候,程序会执行到第一个yield,然后返回{ value:1, done:false },表示yield后面返回1,但是函数Hello还没执行完,函数既不会退出,也不会往下执行。
  3. 当再次执行next的时候,从上次中断的地方接着执行,直到下一个yield或者函数结尾。

正是这种在单个函数内分步执行性质的引入,使得我们能够通过它来完成异步操作的"优化"。

假设有这样的例子

function delay(time, cb){
 setTimeout(function(){
   cb && cb()
 },time);
}

delay(200,function(){
  console.log('200ms done');
  delay(1000,function(){
    console.log('1200ms done');
    delay(500,function(){
       console.log('finish');
     });
  });
});

如何优化这个例子呢?

思路:根据generator的特性,如果我构造一个generator函数包含这三个异步操作,并且把他们各自的callback函数都设置为执行next()函数,这样不就可以实现"看起来是同步"的了吗?

function cl(){
  yieldDelay.next();
}

function* YieldDelay(){
  yield delay(3200,cl);
  console.log('3200ms done!');
  yield delay(4400,cl);
  console.log('4400ms done!');
  yield delay(5500,cl);
  console.log('5500ms done!');
}

var yieldDelay = YieldDelay();
yieldDelay.next();

ok。我们已经迈出了一大步了。不过这个写法看着还是有些别扭。

  1. 第一次执行需要我手动出发next()函数。
  2. 回调函数只是简单地执行next()函数,为什么不能把它更加抽象化,以至于不用定义这个回调函数呢?
    让我们先激动一小会儿,因为你在走tj大神曾经走过的路!

进一步优化这段代码

我们先想想思路,到底有什么办法能够做到呢?最开始的写法之所以会导致金字塔现象,是因为:函数a的执行里面包含执行函数b,所以函数b的执行里面也必须包含执行函数c……如果我们在函数a执行的时候只返回一个function,而这个function接收函数b作为参数。ok,我们先按照这个思路改造一下delay函数和generator函数

function delay(time){
  return function(fn){
    setTimeout(function(){
      fn();
    },time)
  }
}

co(function* (){
  yield delay(4200);
  yield delay(4000);
  yield delay(3000);
})(function(){
  // 回调函数
  console.log('all done!');
})

function co(GenFunc){
   return function(cb){
      //......先略过
   }
}

我们分析一下:

  1. co函数接收generator函数作为参数,然后返回一个函数,该函数接收回调函数。
  2. delay函数接收时间作为参数,返回一个函数,该函数接收回调函数。

再次理一下思路,我们应该如何编写//........先略过这一部分的内容呢?
yield特性可以让我们分阶段执行,暂停→开始→暂停→开始……**如果我们可以让第一次执行的结果是一个函数,而这个函数接收第二次执行本身作为cb函数,第二次执行的结果也是一个函数,而这个函数接收第三次执行本身作为cb函数……直到结束。好吧,说再多还不如来几行代码!

function co(GenFunc) {
  return function(cb) {
    var gen = GenFunc();  // 第一次执行的时候构造出对象
    next()    // 调用自定义的next方法
    function next() {
      var ret = gen.next();   
     // 在generator函数中走一步,delay函数返回一个函数赋给ret.value
      if (ret.done) {    
        // 判断ret.done是否为真,如果为真,说明generator函数执行完了,该调用回调函数了
        cb && cb();
      } else {
      // 如果ret.done为假,那么调用上一个返回的函数,并且把next函数传递给它作为回调函数
        ret.value(next);
      }
    }
  }
}

嗯,看起来有点绕,多看几遍就好了。
至此,你已经山寨了一个极其简单的co库。
当然tj的co库比这个复杂多了,但是原理就是这样,还可以传参数,支持promise

遗留问题:

  1. 该看看ES6原生支持的promise对象了。
  2. generator+co这样的模式确实可以优雅地解决金字塔问题,不过ES7中提供async函数,利用它,不需要依赖co库,也一样可以解决这个问题。

参考资料:

  1. http://es6.ruanyifeng.com/#docs/generator
  2. http://bg.biedalian.com/2013/12/21/harmony-generator.html
  3. http://www.ruanyifeng.com/blog/2015/04/generator.html
@feibenren
Copy link

发现一个很有意思的现象

//这样不会报错
function delay(time, cb){
 setTimeout(function(){
   cb && cb()
 },time);
}

//这样会报错,为什么???
function delay(time, cb){
console.log(time);
 cb && cb()
}






@youngwind
Copy link
Owner Author

@feibenren 报什么错呢?请提供完整的例子和错误信息,不然没有任何帮助。
如何我猜得不错的话,你是指下面这样的情况?但是我实际尝试,并未报错。
es6_generator_ yield_ co_ issue__49 _youngwind_blog

@feibenren
Copy link

feibenren commented Jun 28, 2018

@youngwind ,是这样的,我照着你的例子本地运行了一下,但是我想实现的效果是,不管传入给delay的时间是多少,我都要立马执行,然后我就把delay函数给修改了一下

function delay(time, cb) {
  setTimeout(function() {
    cb && cb();
  }, 0);
}

这样就会立马执行,全部代码如下:

function cl() {
  yieldDelay.next();
}

function delay(time, cb) {
  setTimeout(function() {
    cb && cb();
  }, 0);
}

function* YieldDelay() {
  yield delay(3200, cl);
  console.log("3200ms done!");
  yield delay(4400, cl);
  console.log("4400ms done!");
  yield delay(5500, cl);
  console.log("5500ms done!");
}

var yieldDelay = YieldDelay();
yieldDelay.next();



//////////////
$ node test.js
3200ms done!
4400ms done!
5500ms done!

效果达到了,都会立即执行(当然这样没啥实际意义,直接用最简单的函数也可以实现)
后来我又看了delay函数

function delay(time, cb) {
  setTimeout(function() {
    cb && cb();
  }, 0);
}
//能不能简写成
function delay(time, cb) {
    cb && cb();
}
因为反正都是立即执行,要不要定时器无所谓,但是就这么改了一下,反而报错了

$ node test.js
F:\test\test.js:2
  yieldDelay.next();
             ^
TypeError: Generator is already running
    at YieldDelay.next (<anonymous>)
    at cl (F:\test\test.js:2:14)
    at delay (F:\test\test.js:6:9)
    at YieldDelay (F:\test\test.js:13:9)
    at YieldDelay.next (<anonymous>)
    at Object.<anonymous> (F:\test\test.js:22:12)
    at Module._compile (module.js:652:30)
    at Object.Module._extensions..js (module.js:663:10)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)

这就不明白什么意思了?
定时器在这里到底有啥用??????

@youngwind
Copy link
Owner Author

@feibenren 这现象的确有意思,我并未在官方文档中找到答案,但是下面的这些资料,也许能回答这个问题。

  1. 同步 yield 的坑, By 次碳酸钴
  2. Why does generator's .next need a setTimeout?

我的理解跟上述资料相同:yield 和 next 必须是交替进行的,也就是说,当 yield 执行还没有结束的时候,是不能执行 next 的。在调用 next 之前,会判断 yield 是否已经 return ,如果没有,则报错。在你举出的例子中,就相当于在 yield 里面调用 next 了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants