ES6 generator函数与co一瞥
发布在ES6 generator函数与co一瞥2015年1月27日view:6189YiksiAssowES6
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

enter image description here首发地址:Jim Liu’s Blog

最近开始学(其实就是玩)ES6里的generator/yield,以及传说中的co

首先,我不会Python,所以这是第一次接触generator/yield这种非阻塞编程方式。其次,我虽然知道也很喜欢C#中的async/await,虽然了解一点coroutine/goroutine,但是都没用这两种方式写过正经代码,所以应该说不会受它们影响太多。

话不多说先来看一看generator函数。

JS里的generator函数是一种特殊类型的函数,通过

function* gen(){
  // ...
}

来声明一个generator函数,它和普通函数不一样,虽然在generator函数里也可以return,但是实际上generator函数的返回值是一个迭代器,所以generator函数是一个生成迭代器的函数,相信这就是generator function名字的由来吧。 这里举一个最简单的例子

function* simpleGen(){
  return 'hehe';
}
var iter = simpleGen();

iter就是一个迭代器,我们可以通过next()所返回的“迭代指针”来迭代,比如:

var it = iter.next();
console.log(it.value); // 'hehe'
console.log(it.done);  // true

好嘛,因为上面的simpleGen里面直接return了,所以所谓迭代其实只是看了个最终结果。 那么问题来了,怎么才能让它被迭代起来呢!! 这时候就要配合yield使用了,yield的意思就是“让步”,在它跟C#里面的yield return差不多。外部调用一次调用next,内部进行一步迭代。每一次yield就是所谓的一步,这时迭代器将会暂停工作,并保留所有现场。而代码执行的机会会被让给外部,直到再次next,迭代将会继续。

function* gen2(){
  yield 1;
  yield 2;
  return 'hehe';
}
var iter = gen2();
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 2, done: false }
iter.next(); // { value: 'hehe', done: true }

这个迭代器的概念很像STL里的迭代器,有木有?但是,这时候你会说这特么手工next也能叫迭代?好的,ES6提供了for of语法

for (var it of gen2()){
  console.log(it);
}

上面的代码会输出1和2,但是不会输出'hehe',我不知道是设计如此还是暂时没实现……而且资料上显示的是for (let xx of xxx)才对啊导演。 算了不管了,继续。yield字面意思就是“让步”,可以把执代码执行“让”给yield表达式来执行,而不是像写异步回调那样接着往下执行。呵呵呵呵,真是好人啊。yield *后接一个迭代器就可以把执行的机会让给这个迭代器,比如

function* gen1(){
  yield '1-1';
  yield '1-2';
}
function* gen2(){
  yield '2-1';
  yield* gen1();
  yield '2-2';
}

for (var it of gen2()){
  console.log(it);
}

执行结果就是

2-1
1-1
1-2
2-2

那么问题来了,不是说这货能用来控制流程,简化异步代码的编写吗?

答案就是next可以接收一个参数,它会作为这一次迭代的yield表达式在generator function当中的返回值。

因为直到迭代器被再次调用next为止,generator function都是处于“让步”状态,所以这段时时间内其实可以做任何操作,不论是同步的还是异步的

所以如果我们发现yield表达式的返回值是一个异步操作,比如thunkPromise迭代器generator function,那就意味着这个操作还没有真正执行完

那么问题就简单了,yield不知道它是异步的,但是我们知道啊,甚至我们可以“万物皆异步”,我们可以让异步操作结束后再调用next,从而实现~~化腐朽为神奇~~变异步为同步。

function randomDelay(){
  var time = Math.random() * 500;
  return function(callback){
    setTimeout(callback.bind(this, time), time);
  };
}
function* genSlowly(){
  for (var i=0; i<10; ++i){
    console.log(i);
    console.log(yield randomDelay());
  }
}
async(genSlowly);

通过上面的代码我们希望实现打印一个数,调用一个异步操作randomDelay(),它的作用是随机延迟一段时间(你可以把它YY成一个ajax请求),然后通过回调函数的方式返回这个延迟毫秒数,在外层的genSlowly()函数能够拿到这个返回值,并且打印。 于是大概是这么个意思……

function async(gen){
  var iter = gen();
  function nextStep(it){
    if (it.done) return; // 迭代已完成
    if (typeof it.value === 'function'){
      // 收到的是一个thunk函数,需要等它完成的时候再继续迭代
      it.value(function(ret){
        nextStep(iter.next(ret)); // 把thunk的回调参数传入next,作为yield表达式的返回值
      });
    }else{
      // 收到的是一个值,进行下一步迭代
      nextStep(iter.next(it.value));
    }
  }

  nextStep(iter.next()); // 开始迭代
}

呵呵呵呵,成功了,虽然看起来很弱的样子。

通过对一个generator函数进行“处理”,我们可以改变它本身“迭代器生成器”的作用,用来做流程控制,这听起来真是相当蛋疼啊。不知道是谁发明的,但真是个很有创意的想法。

这时候co就不难理解了,它可以将一个generator函数处理成一个异步操作。这样你可以在generator函数里面使用yield来实现“顺序调用,异步执行”的效果,。在co的4.0版本里它完全采用了Promise,它会将最终返回值作为参数传递到promisethen当中。

例子:

function someThingSlow(callback){
  setTimeout(callback, 500);
}
co(function* fibonacciGenerator(){
  var p1 = 0, p2 = 1;
  while (true){
    var cur = p1 + p2;
    console.log(cur);
    p1 = p2;
    p2 = cur;

    yield someThingSlow;
  }
});
// 每隔一秒打印斐波那契数列,无限

再来个例子,JS程序员梦寐以求的sleep

function sleep(ms){
  return function(callback){
    setTimeout(callback, ms);
  };
}
co(function* (){
  console.log('1');
  yield sleep(1000);
  console.log('2');
});

呵呵呵呵,就是这么无聊……

但是!co之所以这么火并不是没有原因的,当然不是仅仅实现sleep这么无聊的事情,而是它活生生的借着generator/yield实现了很类似async/await的效果!这一点真是让我~~三观尽毁~~刮目相看。

至于具体怎么用,受篇幅限制,还是等下一篇文章再详细说明吧。嗯,我相信你已经感觉到这是又一个《有生之年》系列了(逃

评论
发表评论
16天前
添加了一枚【评注】:迭代器是什么呢?
16天前
添加了一枚【评注】:迭代器是什么呢?
3年前
赞了此文章!
3年前

@刘骥-JimLiu 嗯你说的很有道理,我觉得咱俩的分歧在于竞争与同步互斥是不是由coroutine引起的,如果明确告诉开发者:现在你有coroutine可用,而且它是单线程的所以不用考虑同步问题,那么这个问题也并非无解的。最后结果就是lua或者fibjs这种形式。

3年前

@Kyrios 我原话是:“不带runtime实现去理解coroutine,就不能简单地忽略coroutine之间是并行执行的可能,有并行的可能就必须要谈同步和锁,那么这么一来,就把JS搞复杂了。”,我想表达的是JS是一门很简单的语言,它没有锁这一点是他很简单的一个重要原因,这大大降低了JS开发者大脑的压力。

但如果我们要用coroutine的概念去解释一个形式上看着有那么一点像coroutine的语法,就是用复杂的东西去套简单的东西,反而引入更多不安。——本来我安安心心写无锁的程序就好了,结果还要担心coroutine会不会并行,会不会竞争,会不会有同步和互斥的问题?

在我看来,没这个必要,只要理解这么写会把异步代码写成同步的就OK了,反正它不是coroutine,我没有也不应该拿coroutine来套它。

3年前

@刘骥-JimLiu 谁规定coroutine必须是并行的,coroutine字面意思就是可以同时存在多个执行路径,支持调度和切换而已;

另外并行的确需要同步和锁,但那是在访问全局变量的时候,防止读写冲突;而coroutine本身并不存在这个问题,因为取值、赋值等操作不会被打断,只有显式yield才会中止执行;

所以说就算js有了多路并发,存在锁问题,那也不能赖coroutine,而是多线程并行调用coroutine的机制导致的,就像erlang

goroutine也是这个道理,如果golang本身不支持多线程,那么使用goroutine根本就不用考虑互斥

对fibjs来说,因为js本身不存在互斥锁,那么就无法解决多路执行导致的读写冲突问题。

所以,需要锁这件事情跟coroutine无关,跟并行有关,本质上coroutine和并行没有直接关联。对于程序员来说,有一种将异步的业务逻辑串行写下来这种形式才是最重要的,具体底层是否真的被并行执行了,还是模拟的多线程,没什么区别

3年前

@Kyrios 为什么本质不重要呢?JS之所以简单就是因为它的执行体是不并行的,coroutine的概念脱胎于进程和线程,生而是并行的,或者说至少是并发的,不带runtime实现去理解coroutine,就不能简单地忽略coroutine之间是并行执行的可能,有并行的可能就必须要谈同步和锁,那么这么一来,就把JS搞复杂了。因此就可以说JS就是一门串行语言,这给了它足够的简单性,没什么不好承认的。fibjs的coroutine或者叫fiber也只是runtime级别的,不是JS执行体层面上的,它的JS执行体也是严格串行的,因此它也不需要锁,就是这么一回事啊。说到go就多说两句,goroutine是明确的语言层面的coroutine,程序员明确知道goroutine是可能并行执行的,程序员知道程序需要同步和锁。JS程序员不需要知道,而如果在JS语言形式上说这是coroutine,就会带来一个疑惑,我的代码是并行的吗?那为什么JS没有给我提供同步和锁的基础设施或者标准库呢。

3年前

@刘骥-JimLiu 此外支持yield和支持coroutine还不是绝对等价的,我只见过两个真正称得上coroutine的,一个是lua,一个是fibjs,可以把异步过程写成同步代码。golang的设计理念不是靠纤程实现异步的,而是靠观察者模式;靠纤程而且有多线程的是erlang

3年前

@刘骥-JimLiu 对于generator来说,其实是有一个独立的调用现场的,也许实现上可以把generator转换为多步函数调用+cps,但我觉得不能死抠底层实现上的本质,应用层的形式还是还是很重要的。真要抠实现的话,大家都是一堆汇编指令

3年前

@Kyrios 因为yield/generator本质上只是CPS变换而已,对程序执行流程没有任何影响,JS还是那个JS,用regenerator把yield/generator的代码展开看看就知道了。没有什么coroutine的事儿……

3年前

@Kyrios 协程本来就和线程没有必然关系,我的意思是说JS runtime并没有让JS执行体并发,即同一个时间点只有一个JS执行体正在执行,这是严格的。但如果把协程的概念放到JS执行体上,觉得一个异步请求,比如Ajax,比如fs.readFile认为它是在一个独立协程上,这就更混淆了。因为协程是可以并行的(有多个线程承载协程,就可以并行),但event loop是不可以并行的(V8只有一个JS执行线程)。用yield语法看着像go,只是纯语义上的,执行的时候完全只当它是单线程的就可以,没必要把协程的概念往JS里套,那样反而复杂了,JS还是就当一个单线程语言来用就行。

3年前

@刘骥-JimLiu 我觉得你混淆了协程和线程的概念。没有多线程就不能有协程和多路复用了么

3年前

@Kyrios 说它是coroutine还是不严谨的,因为本质上现在的JS还是单进程单线程单协程语言,不过是异步IO有多路复用,这是runtime多线程,JS的执行线程还是只有一个。据我了解现在的浏览器和V8都只有一个JS线程,Worker的实现机制不了解,但考虑到Worker和主线程之间是只能通信、share nothing的,它不会把这里的问题变复杂。 比如node.js,它的事件循环里每个时间点只有一个JS执行体在执行,宏观上并发,微观上严格串行,因此JS才几乎不需要锁。 所以yield实现的同步代码依然没有改变这一点,没有多个coroutine一说。JS代码依然同一时刻只有一个执行体在运行,依然不需要锁。 这和有真正coroutine的语言来比是有差别的,比如go,虽然go也有1:1调度模型,但是runtime不保证具体有多少个线程在分担goroutine,很可能不同goroutine执行的时候是并行的(多核机器),更关键的是goroutine不是原子的(相对应的,我们都相信JS的event loop对于每个执行体而言是原子的),于是go需要锁。 JS的这种严格的同一时刻只有一个执行体在运行的机制,让JS几乎不需要考虑锁的问题,降低了程序员的头脑压力,但是对多核的利用就只能用cluster这类的方案来实现了。 fibjs的话,我了解比较少,根据我粗浅的了解,fibjs也是单线程的,因为它是基于单一V8实例的。所以fibjs中的fiber是给runtime用的,不是给JS层面实现了fiber。所以fibjs中的JS部分还是严格的单线程单协程的程序,与node.js不同的是对于异步IO,fibjs用的是runtime fiber,node用的是线程池(当然它们的底层异步IO即使是同一套也与此无关)。

3年前

异步流程的同步化只是一个表象,借助yield能够实现coroutine的流程控制,从而可以同时调度多个业务逻辑,这才是最有意思的。

比如写ai脚本,现在就可以把两个策略写成两个纤程,通过调度器调度它们让它们pk,这样就是同时运行两个coroutine了;类似的可以同时调度多个coroutine

3年前
赞了此文章!
3年前
赞了此文章!
WRITTEN BY
刘骥-JimLiu
人称吉姆/刘指导,geek程序员,2B青年,文艺屌丝,百度无线前端码民。jimliu.net
TA的新浪微博
PUBLISHED IN
ES6 generator函数与co一瞥

介绍ES6中的generator/yield的基本应用,co的实现原理,以及如何用它们来改善异步代码的编写难题。

我的收藏