Chinaunix首页 | 论坛 | 博客
  • 博客访问: 386240
  • 博文数量: 69
  • 博客积分: 1984
  • 博客等级: 上尉
  • 技术积分: 953
  • 用 户 组: 普通用户
  • 注册时间: 2007-03-28 00:43
个人简介

学无所长,一事无成

文章分类

全部博文(69)

文章存档

2015年(19)

2014年(14)

2013年(9)

2012年(17)

2010年(10)

我的朋友

分类: JavaScript

2015-06-09 11:40:30

原文地址:

更新: 自本文第一次发布以来,send() 方法已经从标准中移除。send(value) 被 next 方法调用取代,它有一个参数,如 next(value). 本文已修订反映此变化。

长远趋势来看  会进入 Javascript,V8 已装载此特性。这非常激动人心!随后你可能就会看到它的大爆发。但是等等,它到底是什么东西?本文试图尽最大努力在最基础层面对它进行解释。

译注:generators 现在已经是 ES6 的标准,co 库即基于此,只是目前可能尚有待完善。如果求稳,可以采用基于 promise 的库如 q,bluebird 等解决回调陷阱。

安装

开始前,让我们确保能够运行下面例子程序 - 因为代码不是用来看的。你需要安装一个试验版本的 Node (0.11.10+) - 译注:直接装正式版本即可,Node 已经到 0.12.4 版本了。操作如下:

  1. 首先安装
  2. 然后用 nvm install v0.11.10 安装 Node
  3. 现在可以运行例子程序了,确保你使用了--harmony 参数,如 node --harmony 或 node --harmony myscript.js

基础

最简单的形式,一个 generator 就代表一个值的序列 - 也就是说它基本上就是个迭代器 iterator。实际上,一个 generator 对象的接口就是个 iterator,你会在其上不断调用 next() ,直到把值取完。generator 的神奇魔力来自以下写法:

  1. function* naturalNumbers(){ 
  2.   var n = 1;
  3.   while (true){
  4.     yield n++;
  5.   }
  6. }

 


这个例子中(),naturalNumbers 是一个 generator 函数 - 或简单称之为 generator 。这里使用了一个新的 * 语法来定义,同时 ES6 中引入了一个新的 yield 关键字。这个 generator 函数返回了一个 generator 对象,而这个对象又是返回一个自然数的集合,因此它是一个无穷大的序列。每次调用 yield,yielded 的值都会变成序列中的下一个值。要迭代这个序列,ES6 中提出了一种 for-of 语法,当然在 V8 中还没有实现:

  1. // 这段代码还不能运行。这是一个无限循环。
  2. for (var number of naturalNumbers()){
  3.   console.log('number is ', n);
  4. }
 


也别烦恼,我们已经可以用它干些漂亮活了。要创建一个新的序列 sequence,你只需要调用 generator 函数,就可以得到一个活生生的 generator 对象:

> var numbers = naturalNumbers(); 

现在,你可以调用 numbers.next(),然后就得到一个对象,包含属性 value 和 done。

> numbers.next()
{ value: 0, done: false } 

value 就是序列 sequence 中的下一个值,done 用来指明 sequence 是否已取完 - 代码运行到 generator 函数的最后时 sequence 就停止了。当然在自然数这个例子中,sequence 永不终止,因此要解释一个有限的 sequence ,我们先构造一个简单的 sequence ,只返回两个数字:

  1. function* two(){
  2.   yield 1;
  3.   yield 2;
  4. }
 


现在,运行它:

> var seq = two()
> seq.next()
{ value: 1, done: false }
> seq.next()
{ value: 2, done: false }
> seq.next()
{ value: undefined, done: true } 


正如你所见,第三次调用时我们得到值中 done 为 true,value 为 undefined。如果我们再调用第四次,就会得到一个异常 exception:

> seq.next()
Error: Generator has already finished 

代码悬停

现在你已经学习了 generators 的基础知识,用它实现了 iterators。但是 generators 最重大的一个特性是:他可以挂起一段代码的执行。一旦你实例化一个 generator 对象,你就拥有了一个函数句柄,可以任意启动或停止,更进一步无论其何时停止,你都完全可以控制将其重启。为了讲得更具体些,我们创建一个 generator ,其功能只是简单的输出字符串到 console。

  1. function* haiku(){
  2.   console.log('I kill an ant');
  3.   yield null; // 因为 yield 关键字需要一个参数,所以我们放个 null 在后面
  4.   console.log('and realize my three children');
  5.   yield null;
  6.   console.log('have been watching.');
  7.   yield null;
  8.   console.log('- Kato Shuson');
  9. }

现在,如果通过这个 generator 来进行迭代


> var g = haiku() 

只需要在命令行一次又一次地调用 g.next()

> g.next() 

你要理解如下事实

  1. generator 中的代码除非你调用它,否则根本不会开始执行
  2. 当代码遇到一个 yield 状态,它将无限挂起,直到你再次调用才会继续

这就为异步编程提供了一种可能:你可以在某种异步事件触发后才调用 next()。为加深理解,我们看看下面这个例子,这个例子通过结合 generator 和一个异步循环将一首诗每秒显示一行。

  1. var g = haiku();
  2. function next(){
  3.   if (g.next().done) return;
  4.   setTimeout(next, 1000);
  5. }
  6. next();
 


你也许注意到了 generator haiku 中的代码也就跟一排排线性的 Javascript 代码差不多,但是在代码运行的中间产生了异步调用 - 由于 Javascript 通常是单线程的本质,这在以前是不可能的。更为特殊的是,每一次遇到 yield,我们都有机会触发一次异步调用。这些 yield 就像是扭曲时间的虫洞。()

发送数据

到目前为止我们只是将 generator objects 看做一个值序列的产生器,信息传送只有一条路 - 从 generator 传送到你这里。但实际证明你也可以通过给 next() 传送一个参数将数据发送回去,在这种情况下 yield 语句会有一个实际的返回值!我们构造一个 generator 来消费 (consumers) 这些值:

  1. function* consumer(){
  2.   while (true){
  3.     var val = yield null;
  4.     console.log('Got value', val);
  5.   }
  6. }

 

首先实例化一个 generator 对象

> var c = consumer() 

接下来,带参数调用 next() :


> c.next(1)
{ value: null, done: false } 

返回值正如预期是个 object,但console.log() 似乎没有动作,原因在于初始化时 generator 还没有 yielding。如果我们再次带参数调用,就能看到 console.log 的输出信息了:

> c.next(2)
Got value 2
{ value: null, done: false }
> c.next(3)
Got value 3
{ value: null, done: false } 

抛出异常

酷!现在我们已经可以同 generator 收发数据了,猜猜看,接下来干啥?你可以抛出 throw 异常!

> c.throw(new Error('blarg!'))
Error: blarg! 

当你向  generator object 上抛出 throw() 一个 error,error 实际上会传播到 generator 代码内部,这意味着你可以使用 try 和 catch 语句捕获它。那么我们在上一个例子中添加 try/catches 看看:

  1. function* consumer(){
  2.   while (true){
  3.     try{
  4.       var val = yield null;
  5.       console.log('Got value', val);
  6.     }catch(e){
  7.       console.log('You threw an error but I caught it ;P')
  8.     }
  9.   }
  10. }


这一次我们一旦初始化好 generator object,就先调用一次 next(),因为 generator 没法捕获一个在他运行之前抛出的 error。

> var c = consumer()
> c.next() 

从这往后,一旦我们 throw 一个 error,它都会干净漂亮的捕获并处理掉:

> c.throw(new Error('blarg!'))
You threw an error but I caught it ;P 

try/catch worked.

Getting Fancy

现在你已经知道 generators 的基本原理了,你能用它做点实在的不?哦,很多粉丝估计已经激动起来,他们似乎看到了逃离调地狱的船票了?让我们看看怎么干的。

The Premise

在 Javascript 中,特别是 Node 中, IO 操作一般都是异步操作,都需要一个回调函数。当你不得不一个接一个进行多重的异步操作时,代码会看起来像这样:

  1. fs.readFile('blog_post_template.html', function(err, tpContent){
  2.   fs.readFile('my_blog_post.md', function(err, mdContent){
  3.     resp.end(template(tpContent, markdown(String(mdContent))));
  4.   });
  5. });

当加入 error 处理时会更糟糕:
  1. fs.readFile('blog_post_template.html', function(err, tpContent){
  2.   if (err){
  3.     resp.end(err.message);
  4.     return;
  5.   }
  6.   fs.readFile('my_blog_post.md', function(err, mdContent){
  7.     if (err){
  8.       resp.end(err.message);
  9.       return;
  10.     }
  11.     resp.end(template(tpContent, markdown(String(mdContent))));
  12.   });
  13. });

 generators 的一个承诺就是你现在可以使用 generators 写出线性风格的等效代码
  1. try{
  2.   var tpContent = yield readFile('blog_post_template.html');
  3.   var mdContent = yield readFile('my_blog_post.md');
  4.   resp.end(template(tpContent, markdown(String(mdContent))));
  5. }catch(e){
  6.   resp.end(e.message);
  7. }


太神奇了!除了更少的代码和更好的心情外,它还带来如下好处:

  • 每一行独立:每一个具体操作不再同其后的一堆操作绑定在一起。如果你想调整操作顺序,简单地交换行就行了。如果你想取消某个操作,简单地删除那一行就行了。
  • 更简单以及符合 DRY 的错误处理:基于回调风格编码时,你需要为每一个独立的异步操作处理 error,而基于 generator 风格编码时,你只需要在所有操作外面加一个 try/catch 代码块,就可以统一地处理 errors - generators 重新将 try/catch 异常处理的威力还给我们了。

Make it Happen

你已经看到了我们的目标所在,现在我们该如何实现它?如果你充满冒险精神,想要孤身一人揭示未知领域,我可不想剥夺你的乐趣。停下来,不要读了,编码去。一旦你想回来,我会在这里等你。

需要认清的第一件事是异步操作都发生在 generator 函数外面。这意味着需要某种类型的 "controller" 来处理调度 generator 的运行,来回填 fulfill 异步请求,并且返回处理结果。因此我们需要将 generator 传递给这个 controller,基于此我们只需创建一个 run() function:

  1. run(function*(){
  2.   try{
  3.     var tpContent = yield readFile('blog_post_template.html');
  4.     var mdContent = yield readFile('my_blog_post.md');
  5.     resp.end(template(tpContent, markdown(String(mdContent))));
  6.   }catch(e){
  7.     resp.end(e.message);
  8.   }
  9. });

 

run() 可以通过 next() 反复调用 generator object ,
and fulfill a request each time a value is yielded. It will assume that the requests it receives are functions that take a single callback parameter which takes an err, and another value argument - 同 Node 类型的回调风格保持一致。当出现 err ,它会调用 generator 上的 throw() on the generator object to propagate it back into the generator's code path. run() 代码类似如下:

  1. function run(genfun){
  2.   // 实例化 generator object
  3.   var gen = genfun();
  4.   // 异步循环模式
  5.   function next(err, answer){
  6.     var res;
  7.     if (err){
  8.       // 如果 err,抛入到虫洞里
  9.       return gen.throw(err);
  10.     }else{
  11.       // 如果是 good value, 发送
  12.       res = gen.next(answer);
  13.     }
  14.     if (!res.done){
  15.       // if we are not at the end
  16.       // we have an async request to
  17.       // fulfill, we do this by calling 
  18.       // `value` as a function
  19.       // and passing it a callback
  20.       // that receives err, answer
  21.       // for which we'll just use `next()`
  22.       res.value(next);
  23.     }
  24.   }
  25.   // Kick off the async loop
  26.   next();
  27. }

 

Now given that, readFile takes the file path as parameter and needs to return a function

  1. function readFile(filepath){
  2.   return function(callback){
  3.     fs.readFile(filepath, callback);
  4.   }
  5. }


And that's it! If that went too fast, feel free to poke at the full source code.

更多资源

学习更多 generators 知识:

  • The original post by  when generators made it into V8
  •  和 他的 ,其原则非常类似上面我演示的 run function
  •   采用了略微不同的方法,同 co 比有着某些不同的权衡
  •  - Dave Herman 的 task library
  •  是一种支持 generators 的 promise library
  • A  on generators in python by David Beazley
  • The original proposal that introduced generators to Python
Ever feel like you are wasting your life away debugging?
It doesn't have to be this way. Check out my .
阅读(1056) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~