学无所长,一事无成
分类: 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 版本了。操作如下:
最简单的形式,一个 generator 就代表一个值的序列 - 也就是说它基本上就是个迭代器 iterator。实际上,一个 generator 对象的接口就是个 iterator,你会在其上不断调用 next() ,直到把值取完。generator 的神奇魔力来自以下写法:
这个例子中(),naturalNumbers 是一个 generator 函数 - 或简单称之为 generator 。这里使用了一个新的 * 语法来定义,同时 ES6 中引入了一个新的 yield 关键字。这个 generator 函数返回了一个 generator 对象,而这个对象又是返回一个自然数的集合,因此它是一个无穷大的序列。每次调用 yield,yielded 的值都会变成序列中的下一个值。要迭代这个序列,ES6 中提出了一种 for-of 语法,当然在 V8 中还没有实现:
也别烦恼,我们已经可以用它干些漂亮活了。要创建一个新的序列 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 ,只返回两个数字:
现在,运行它:
> 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。
> var g = haiku()
只需要在命令行一次又一次地调用 g.next()
> g.next()
你要理解如下事实
这就为异步编程提供了一种可能:你可以在某种异步事件触发后才调用 next()。为加深理解,我们看看下面这个例子,这个例子通过结合 generator 和一个异步循环将一首诗每秒显示一行。
你也许注意到了 generator haiku 中的代码也就跟一排排线性的 Javascript 代码差不多,但是在代码运行的中间产生了异步调用 - 由于 Javascript 通常是单线程的本质,这在以前是不可能的。更为特殊的是,每一次遇到 yield,我们都有机会触发一次异步调用。这些 yield 就像是扭曲时间的虫洞。()
到目前为止我们只是将 generator objects 看做一个值序列的产生器,信息传送只有一条路 - 从 generator 传送到你这里。但实际证明你也可以通过给 next() 传送一个参数将数据发送回去,在这种情况下 yield 语句会有一个实际的返回值!我们构造一个 generator 来消费 (consumers) 这些值:
首先实例化一个 generator 对象
> var c = consumer()
> 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 看看:
这一次我们一旦初始化好 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.
现在你已经知道 generators 的基本原理了,你能用它做点实在的不?哦,很多粉丝估计已经激动起来,他们似乎看到了逃离回调地狱的船票了?让我们看看怎么干的。
在 Javascript 中,特别是 Node 中, IO 操作一般都是异步操作,都需要一个回调函数。当你不得不一个接一个进行多重的异步操作时,代码会看起来像这样:
太神奇了!除了更少的代码和更好的心情外,它还带来如下好处:
你已经看到了我们的目标所在,现在我们该如何实现它?如果你充满冒险精神,想要孤身一人揭示未知领域,我可不想剥夺你的乐趣。停下来,不要读了,编码去。一旦你想回来,我会在这里等你。
需要认清的第一件事是异步操作都发生在 generator 函数外面。这意味着需要某种类型的 "controller" 来处理调度 generator 的运行,来回填 fulfill 异步请求,并且返回处理结果。因此我们需要将 generator 传递给这个 controller,基于此我们只需创建一个 run() function:
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() 代码类似如下:
Now given that, readFile takes the file path as parameter and needs to return a function
And that's it! If that went too fast, feel free to poke at the full source code.
学习更多 generators 知识: