ES6 In Depth 是一个系列,介绍了 ECMAScript 标准第 6 版(简称 ES6)中添加到 JavaScript 编程语言的新特性。
欢迎回到 ES6 In Depth!我希望你在夏季休假期间玩得和我们一样开心。但程序员的生活不可能全是烟火和柠檬水。现在该继续我们之前的工作了,我准备了一个完美的主题来继续。
早在五月份,我就写过一篇关于生成器的文章,这是 ES6 中引入的一种新的函数类型。我称它们是 ES6 中最神奇的特性。我谈到了它们可能是异步编程的未来。然后我又写了这篇文章
关于生成器还有很多要说的……但我想这篇文章已经足够长,也足够令人困惑了。就像生成器本身一样,我们应该暂停,并在下次再讨论其他内容。
现在就是时机了。
您可以在此处找到本文的第一部分。 建议您在阅读本文之前先阅读那篇文章。去吧,它很有趣。它……有点长,有点令人困惑。但是有一只会说话的猫!
快速回顾
上次,我们重点关注了生成器的基本行为。它可能有点奇怪,但并不难理解。生成器函数非常像普通函数。主要区别在于生成器函数的函数体不会一次性执行。它会一次执行一点点,每次执行到yield
表达式时都会暂停。
在第一部分中有一个详细的解释,但我们从未做过一个完整的示例来演示所有部分是如何组合在一起的。现在让我们来做一下。
<pre>
function* someWords() {
yield "hello";
yield "world";
}
for (var word of someWords()) {
alert(word);
}
</pre>
这段脚本非常直观。但如果您可以观察到这段代码中发生的一切,就像所有代码片段都是戏剧中的角色一样,那将是一个截然不同的脚本。它可能像这样
SCENE - INTERIOR COMPUTER, DAY FOR LOOP stands alone onstage, wearing a hard hat and carrying a clipboard, all business. FOR LOOP (calling) someWords()! The GENERATOR appears: a tall, brass, clockwork gentleman. It looks friendly enough, but it's still as a statue. FOR LOOP (clapping her hands smartly) All right! Let's get some stuff done. (to the generator) .next()! The GENERATOR springs to life. GENERATOR {value: "hello", done: false} It freezes in a goofy pose. FOR LOOP alert! Enter ALERT at a sprint, wide-eyed and breathless. We sense he's always like that. FOR LOOP Tell the user "hello". ALERT turns around and sprints offstage. ALERT (offstage, screaming) Stop everything! The web page at hacks.mozilla.org says, "hello"! A few seconds' pause, then ALERT races back on, crossing all the way over to FOR LOOP and skidding to a halt. ALERT The user says OK. FOR LOOP (clapping her hands smartly) All right! Let's get some stuff done. (turning back to the GENERATOR) .next()! The GENERATOR again springs to life. GENERATOR {value: "world", done: false} It freezes in a different goofy pose. FOR LOOP alert! ALERT (already running) On it! (offstage, screaming) Stop everything! The web page at hacks.mozilla.org says, "world"! Again, a pause, then ALERT trudges back onstage, suddenly crestfallen. ALERT The user says OK again, but... but please prevent this page from creating additional dialogues. He exits, pouting. FOR LOOP (clapping her hands smartly) All right! Let's get some stuff done. (turning back to the GENERATOR) .next()! The GENERATOR springs to life a third time. GENERATOR (with dignity) {value: undefined, done: true} Its head comes to rest on its chest and the lights go out of its eyes. It will never move again. FOR LOOP Time for my lunch break. She exits. After a while, the GARBAGE COLLECTOR enters, picks up the lifeless GENERATOR, and carries it offstage.
好吧,它并不完全是《哈姆雷特》。但你明白我的意思。
正如你在戏剧中看到的那样,当生成器对象第一次出现时,它是处于暂停状态的。每次调用它的.next()
方法时,它都会醒来并运行一小段时间。
这个动作是同步的,并且是单线程的。请注意,在任何给定时间,这些角色中只有一个实际上正在做任何事情。角色之间从不互相打断或相互交谈。他们轮流说话,谁在说话就可以说多久就说多久。(就像莎士比亚!)
每次将生成器传递给for
-of
循环时,都会上演这个戏剧的某个版本。总是有这一系列.next()
方法调用,这些调用在你的代码中任何地方都看不见。在这里,我将它全部放在舞台上,但对于你和你的程序来说,这一切都将发生在幕后,因为生成器和for
-of
循环是通过迭代器接口设计为协同工作的。
因此,为了总结到目前为止的所有内容
- 生成器对象是礼貌的黄铜机器人,它们会产生值。
- 每个机器人的程序都包含一个代码块:创建它的生成器函数的函数体。
如何关闭生成器
生成器有一些细微的额外功能,我在第一部分中没有介绍
generator.return()
generator.next()
的可选参数generator.throw(error)
yield*
我跳过它们主要是因为,如果不了解这些功能为什么存在,就很难关心它们,更不用说把它们全部记在脑子里了。但随着我们更多地思考我们的程序将如何使用生成器,我们将看到这些原因。
以下是一个你可能在某个时候用过的模式
<pre>
function doThings() {
setup();
try {
// ... 做一些事情 ...
} finally {
cleanup();
}
}
doThings();
</pre>
清理可能包括关闭连接或文件,释放系统资源,或者只是更新 DOM 以关闭“正在进行”的旋转器。我们希望无论我们的工作是否成功完成,这都要发生,所以它放在finally
块中。
这在生成器中会是什么样子呢?
<pre>
function* produceValues() {
setup();
try {
// ... 产生一些值 ...
} finally {
cleanup();
}
}
for (var value of produceValues()) {
work(value);
}
</pre>
看起来还可以。但这里有一个细微的问题:调用work(value)
不在try
块中。如果它抛出了一个异常,我们的清理步骤会发生什么?
或者假设for
-of
循环包含一个break
或return
语句。那么清理步骤会发生什么?
它照常执行。ES6 会替你处理。
当我们第一次讨论迭代器和for
-of
循环时,我们说过迭代器接口包含一个可选的.return()
方法,当迭代在迭代器说它已经完成之前退出时,语言会自动调用该方法。生成器支持此方法。调用myGenerator.return()
会导致生成器运行任何finally
块,然后退出,就像当前的yield
点神秘地变成了return
语句一样。
请注意,.return()
并非在所有上下文中都由语言自动调用,而是在语言使用迭代协议的情况下才会调用。因此,生成器有可能在从未运行其finally
块的情况下被垃圾回收。
这个功能会在舞台上如何上演呢?生成器被冻结在一个需要一些设置的任务的中间,比如建造一座摩天大楼。突然有人抛出了一个错误!for
循环捕获它并将其搁置一旁。她告诉生成器.return()
。生成器平静地拆除了所有脚手架并关闭了。然后for
循环重新获取错误,并继续正常的异常处理。
生成器掌权
到目前为止,我们看到的生成器与其用户之间的对话都是单方面的。暂时抛开戏剧的比喻
用户说了算。生成器按需执行其工作。但这并不是使用生成器的唯一方式。
在第一部分中,我说过生成器可以用于异步编程。你现在使用异步回调或 promise 链来完成的事情,可以使用生成器来代替。你可能想知道这到底是如何工作的。为什么生成器仅仅能够产生(这毕竟是生成器的唯一特殊能力)就足够了呢?毕竟,异步代码不仅会产生。它会让事情发生。 它从文件和数据库中调用数据。它向服务器发送请求。然后它返回到事件循环中等待这些异步进程完成。生成器到底是如何做到这一点的呢?没有回调,当数据从这些文件和数据库以及服务器中传入时,生成器是如何接收数据的呢?
为了开始找到答案,考虑一下如果我们只有一种方法可以将值传递回生成器,会发生什么。仅仅改变这一点,我们就可以进行一种全新的对话
事实上,生成器的.next()
方法确实接受一个可选参数,巧妙的是,该参数随后会在生成器中显示为yield
表达式返回的值。也就是说,yield
不像return
那样是一个语句;它是一个表达式,一旦生成器恢复,它就有一个值。
var results = yield getDataAndLatte(request.areaCode);
这一行代码做了很多事情
- 它调用
getDataAndLatte()
。假设该函数返回字符串"get me the database records for area code..."
,我们在屏幕截图中看到了这个字符串。 - 它暂停生成器,产生字符串值。
- 此时,任何时间都可能过去。
- 最终,有人调用
.next({data: ..., coffee: ...})
。我们将该对象存储在局部变量results
中,然后继续下一行代码。
为了在上下文中展示这一点,这里提供了上述完整对话的代码
<pre>
function* handle(request) {
var results = yield getDataAndLatte(request.areaCode);
results.coffee.drink();
var target = mostUrgentRecord(results.data);
yield updateStatus(target.id, "ready");
}
</pre>
请注意,yield
仍然只表示它之前的意思:暂停生成器并将值传递回调用方。但事物发生了变化!这个生成器期望它的调用方表现出非常具体的支持性行为。它似乎期望调用方充当行政助理。
普通函数通常不会这样。它们往往是为了满足调用方的需求而存在的。但生成器是你可以与其对话的代码,这使得生成器与其调用方之间可以建立更广泛的可能关系。
这个行政助理生成器运行器可能是什么样子呢?它并不需要那么复杂。它可能像这样。
<pre>
function runGeneratorOnce(g, result) {
var status = g.next(result);
if (status.done) {
return; // 呼!
}
// 生成器要求我们获取某些内容,
// 并在我们完成时将其回调。
doAsynchronousWorkIncludingEspressoMachineOperations(
status.value,
(error, nextResult) => runGeneratorOnce(g, nextResult));
}
</pre>
为了启动,我们需要创建一个生成器并运行它一次,像这样
runGeneratorOnce(handle(request), undefined);
五月份,我提到了Q.async()
,它是一个将生成器视为异步进程并自动运行它们的库。runGeneratorOnce
就是这种东西。实际上,生成器不会产生字符串来说明它们需要调用方做什么。它们可能会产生 Promise 对象。
如果你已经了解了 promise,现在你也了解了生成器,你可能想尝试修改runGeneratorOnce
以支持 promise。这是一项艰巨的任务,但一旦你完成,你就可以使用 promise 将复杂的异步算法写成直线代码,而不是.then()
或回调。
如何炸毁一个生成器
你注意到runGeneratorOnce
是如何处理错误的吗?它忽略了它们!
好吧,这不好。我们真的希望以某种方式将错误报告给生成器。生成器也支持这一点:你可以调用generator.throw(error)
而不是generator.next(result)
。这会导致yield
表达式抛出异常。就像.return()
一样,生成器通常会被杀死,但如果当前的yield点在一个try
块中,那么catch
和finally
块会被执行,所以生成器可能会恢复。
修改runGeneratorOnce
以确保.throw()
被适当地调用是另一个很好的练习。请记住,在生成器中抛出的异常总是会传播给调用者。所以generator.throw(error)
会直接将error
抛回到你那里,除非生成器捕获了它!
这完成了生成器到达yield
表达式并暂停时的所有可能性。
- 有人可能会调用
generator.next(value)
。在这种情况下,生成器将从它停止的地方恢复执行。 - 有人可能会调用
generator.return()
,可选地传递一个值。在这种情况下,生成器不会恢复它正在做的事情。它只会执行finally
块。 - 有人可能会调用
generator.throw(error)
。生成器表现得好像yield
表达式是一个调用抛出error
的函数。 - 或者,可能没有人会做任何这些事情。生成器可能会永远冻结。(是的,生成器可以进入一个
try
块,并且根本不执行finally
块是可能的。生成器甚至可以在这种状态下被垃圾回收器回收。)
这与普通的函数调用相比并没有复杂多少。只有.return()
是一个真正的新可能性。
事实上,yield
与函数调用有很多共同之处。当你调用一个函数时,你是暂时暂停了,对吧?你调用的函数处于控制状态。它可能会返回。它可能会抛出异常。或者它可能会永远循环。
生成器协同工作
让我展示另一个特性。假设我们编写一个简单的生成器函数来连接两个可迭代对象。
<pre>
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
</pre>
ES6 为此提供了一个简写方式
<pre>
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
一个普通的yield
表达式产生一个单一的值;一个yield*
表达式消费一个完整的迭代器并产生所有的值。
相同的语法也解决了另一个有趣的难题:如何在生成器内部调用生成器的问题。在普通函数中,我们可以从一个函数中取出一些代码,并将其重构到一个单独的函数中,而不改变行为。显然我们也希望重构生成器。但是我们需要一种方法来调用重构后的子程序,并确保我们之前产生的每个值仍然被产生,即使是子程序现在正在产生这些值。yield*
是实现这一目标的方法。
<pre>
function* factoredOutChunkOfCode() { ... }
function* refactoredFunction() {
...
yield* factoredOutChunkOfCode();
...
}
</pre>
想象一个黄铜机器人将子任务委派给另一个机器人。你可以看到这个想法对于编写大型基于生成器的项目并保持代码的整洁和组织的重要性,就像函数对于组织同步代码至关重要一样。
退场
好了,这就是生成器!我希望你像我一样喜欢它。很高兴回来。
下周,我们将讨论另一个令人惊叹的功能,它完全是 ES6 中的新功能,一种新型的对象,它非常微妙,非常巧妙,你甚至可能在不知情的情况下使用它。下周请加入我们,深入了解 ES6 代理。
8 条评论