ES6 in Depth: Generators, Continued
欢迎回到 深入ES6。我希望你们能在我们的暑假期间也很快乐,但是编程大大的生活不能总是烟花和柠檬水,现在是时候拾起我的丢下的 —- 重新开始我已经讨论完美的主题(深入ES6)。
在5月份,我写到生成器,这是ES6中的新功能。我在ES6中称它为最为魔法的特性,谈到它是怎么影响将来的异步编程的。然后,我写到:
对于生成器还有更多的东西要说的。… 但是,我认为这文章已经够长的了,现在足够引起疑惑了。就如生成器一般,我们应该暂停一下,在以后的时间再继续 。
现在,是时候了。
你可以在这里看第一部分的内容。在阅读现在这篇文章时,最好先读一下链接中文章。继续,这里很有意思,它有有点长、有点令人困惑。但,它是个会说话的猫(?)。
快速场景
上次,我们关注生成器的基本用法,它也许可能有点奇怪,但是并不难理解。一个生成器函数看起来像个普通的函数,主要的不同点在于生成器函数的内容并不仅仅运行一次。每次运行时,会暂停而去执行yield
的表达式。
这具体的细节已经在之前的文章已经说明,但是,我们并没有做个彻底的把所有部分都整合在一起的例子。让我们现在开始做吧。
1 2 3 4 5 6 7 8 |
|
这脚本够直接的了,但是,如果你注意到代码之间发生的事情,它就像是所有不同代码组成的一台戏,这时就是个不同的脚本了,它可能看起来会是这样:
(略)
这并不是什么哈姆雷特,但你可以想象一下这场景。
就如你在这戏中看到的,当一个生成器对象第一次出现时,它会暂停。每次当方法.next()
被调用时,它就会醒来。
这动作是同步的、单线程的。注意,这里只有一个角色是可以在任意时间做任何事情,这角色不会互相打断对方,也不会与他人进行交互。他们轮流说话,而且,只要他们想,他们就可以想说多久就多久。(就像是莎士比亚!)
在这戏剧的一些版本中,生成器每次相当于被喂食给for-of
循环。在这过程中,会有.next()
方法在按序地执行,但并不会出现在你的任何代码中。这里,我把它放到舞台中,但对你们和你们的程序来说,所有的这些是在后台发生的,因为生成器和for-of
循环在设计时是一起工作的,他们通过这个迭代接口 实现的。
所以,总结这里的点:
- 生成器对象是个听话的机器人,它可以产出值;
- 每次这机器人的程序包括一个简单的代码块,生成器的主体函数会生成这代码块。
怎么关闭一个生成器
生成器有一些非常巧妙的特性,我并没有在第一部分进行说明:
generator.return()
generator.next()
可选参数generator.throw(error)
yield*
我跳过这些主要是因为没有理解为什么这些特性会存在。我们很难注意到这些特性,在你们的脑子里并没有直接的印象。但是,当我们细想我们的程序是怎么使用生成器时,我们就能看到原因。
这里有些你可能会碰到过:
1 2 3 4 5 6 7 8 9 10 |
|
这个cleanup方法可能会关闭连接或者是文件,释放系统资源,或者只是更新DOM去关闭“处理中”的加载图标。我们希望无论我们的工作是否成功完成都能执行它,所以它会出现在finally
代码块中。
那么在生成器中会是怎么样的呢?
1 2 3 4 5 6 7 8 9 10 11 12 |
|
这看起来很好,但是这里有点小问题:work(value)
的调用并不在try的代码块中,如果有异常抛出,我们 cleanup 的时候会发生什么呢?
或者,当 for-of
循环中包含有 break 或者 return 语句时,cleanup的时候又会发生什么呢?
无论它怎么执行,你都可以控制它。
当我们谈到迭代器和for-of循环 时,我们说到迭代器的接口里有一个可选的 .return()
的方法,当每次迭代完成之前它都会被自动调用。生成器也支持些方法。调用myGenerator.return()
方法,会最终运行任意的finally
的代码块,然后退出,就像是当前yield
当作为return的返回语句一样。
注意,.return()
并不会被JS的所有内容所调用,只有当JS使用到了迭代器的协议时才会。所以,一个生成器在没有运行 finally 代码块时有可能会被垃圾回收掉。
为什么这特性没有在剧本中体现呢?当一个任务正在依赖其它步骤完成的时候,生成器会被冻结,就像建造一幢大楼一样。突然,某人抛出了一个错误!for
循环会抓取到它,并把它放到一边,并告诉生成器调用 .return()
方法。生成器会安静地拆除整个建筑架子并关闭它。然后,for
循环会获取到错误,一个普通的异常会继续处理中。
生成器管理
目前,我们看到了生成器与其用户之间的会话的其中一方面,先打断一下之前的剧场类比:
用户是主管,生成器会按照要求工作。但是,这并不是生成器的唯一编程方式。
在第一部分的文章中,我说过,生成器可以用于异步编程,也就是你可以用生成器来替代 异步的返回函数或者 promise 链。你可能会对它如何支持工作感到迷惑,为什么yield的能力那么有效(毕竟这是生成器特殊的能力)?毕竟,异步代码不仅是yield而已,它会触发更多的事情。然后,它返回到循环的事件中,等待处理那些异步进程,并完成它们。生成器是怎么做到这些的?没有返回函数,当数据 从文件、数据库和服务器中 返回时,生成器是怎么接收的呢?
为了接近答案并开始工作,如果我们通过仅能调用的.next()
方法传递参数来给生成器,通过这交易,我们可以看到整个新类型的会话:
生成器的.next()
方法实际上可以接收一个可选的参数,同时,它会聪明地将这参数作为value值返回到生成器中的yield的表达式中。是的,yield 并不类似于 return,这包含于个value值,生成器会获取一次。
1
|
|
上面一行简单的代码做了很多的事情:
- 它会调用
getDataAndLatter()
,此函数会返回一个我们在屏幕中看到的字符串“get me the database records for area code … “。 - 它会暂停生成器,yield 这字符串值
- 这时,许多时间会过去
- 最终,某人调用
.next({data: ..., coffee: ...})
。我们存储相应的对象到本地的变量results
中,并继续下一行代码。
为了展示其内容,下面的代码显示了的整个会话:
1 2 3 4 5 6 |
|
注意,yield 仍然是之前的意思:暂停生成器并传递个value值给调用者。但是,事情发生了变化。生成器希望从其调用者中得到具体的支持性的行为,这意味着其希望调用者扮演着一个管理助手。
普通的函数并不会这样。它们倾向于服务它们调用者的需求。但是,你可以为生成器指定会话的代码,从而加大生成器与其调用者之间的联系。
管理助手式的生成器运行会是什么样?它并不是个复杂的事情,它可能看起来如:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
为了保证运行,我们不得不创建一个生成器并运行一次,类似:
1
|
|
在五月份,我提到Q.async()
就是个例子,这库中的生成器会被看作为异步进程,并在需要时自动运行。runGeneratorOnce
就是做这种事。实际上,生成器不会并不会处理其yield的值,这应该由其调用者来做。生成器只会yield Promise对象。
如果你已经懂得promise,现在又理解了生成器,你可以会想试着修改runGeneratorOnce
来支持promise。这会是个困难的实践,但只要你做成了,你将可以直接使用promise来编写复杂的异步逻辑的算法,而不是.then()
或者一个返回函数了。
怎么扩展生成器
你是否看到runGeneratorOnce
是怎么处理错误的?它忽略了他们。
好的,这并不是好主意。我们总得需要生成器报告错误出来,生成器其实是支持的:你可以调用generator.throw(error)
而不用generator.next(result)
。这会导致yield
的表达式会被抛出,就如.return()
,生成器会被杀死。但是,如果当前yield的地方在一个try代码块中,然后then和finally代码块会被尊重,生成器会继续恢复运行。
修改runGeneratorOnce()
并适应地使用.throw()
是另外一个好的实践,这样可以保证异常在生成器内部抛出,并传播给调用者。所以,generator.throw(error)
会正常抛出 error 错误,除非你在生成器中获取了它。
当一生成器遇到yield
表达式并停止时,可能会有的操作:
- 某些人会调用
generator.next(value)
,这时,生成器会从上次结束执行的地方再次恢复执行。 - 某些人会调用
generator.return()
,传递的值是可选的。这情况,无论生成器在做什么,将不会恢复执行,而只会执行finally
代码块。 - 某些人会调用
generator.throw(error)
,这生成器就会表现如yield
表达式调用一个函数并返回错误一样。 - 或者,没有人做任何事情。生成器会永远处理冻结状态。(是的,这可能会导致一个生成器进入
try
代码块之后,而不会去执行finally
代码块。当生成器处于此状态时,它甚至可以从垃圾回收器那里重新恢复使用。)
一起工作的生成器
让我多展示一个特性,我们可以编写一个简单的生成器函数,作用是连接两个迭代对象集:
1 2 3 4 5 6 7 8 |
|
ES6还支持缩写的方法:
1 2 3 4 |
|
一个普通的yield
表达式会 yield 一个简单值,一个yield*
表达式会消费整个迭代器并yield所有的值。
这个语法还解决另外一个有趣的问题:在一个生成器内部应该怎么调用生成器的问题。在普通的函数中,我们可以从一个函数中抽取一部分的代码,并重构成为一个单独的函数,而不会修改其行为。很明显,我们也将会重构生成器,但是我们需要找出一个方法来调用分解出去的子程序,同时需要确认在每次原有yield值之前,处理好子程序的yield操作,尽管那子程序也会生产出值。yield*
就是处理这情况的方式。
1 2 3 4 5 6 7 |
|
可以类比想象一下,一个管道程序将部分子任务授予给其它进行操作。当编写大型的基于生成器的项目时,需要保持代码的清晰和易管理,你可以看到这方法是十分重要的,就类似函数在同步代码中的重要一般。
退场
好了,这就是生成器。我希望你也能够和我一样喜欢它,很高兴回过头再次提到它。
下周,我们将讨论另外让人脑洞大开的特性,也是ES6中新的特性。这是新类型,它是微小又狡猾的,在你还没有在这里了解它时可能你已经使用到它了。请在下周加入我们,一起开始深入ES6代理。