ES6 in Depth: Generators, Continued

ES6 In Depth: Generators, continued

欢迎回到 深入ES6。我希望你们能在我们的暑假期间也很快乐,但是编程大大的生活不能总是烟花和柠檬水,现在是时候拾起我的丢下的 —- 重新开始我已经讨论完美的主题(深入ES6)。

在5月份,我写到生成器,这是ES6中的新功能。我在ES6中称它为最为魔法的特性,谈到它是怎么影响将来的异步编程的。然后,我写到:

对于生成器还有更多的东西要说的。… 但是,我认为这文章已经够长的了,现在足够引起疑惑了。就如生成器一般,我们应该暂停一下,在以后的时间再继续 。

现在,是时候了。

你可以在这里看第一部分的内容。在阅读现在这篇文章时,最好先读一下链接中文章。继续,这里很有意思,它有有点长、有点令人困惑。但,它是个会说话的猫(?)。

快速场景

上次,我们关注生成器的基本用法,它也许可能有点奇怪,但是并不难理解。一个生成器函数看起来像个普通的函数,主要的不同点在于生成器函数的内容并不仅仅运行一次。每次运行时,会暂停而去执行yield的表达式。

这具体的细节已经在之前的文章已经说明,但是,我们并没有做个彻底的把所有部分都整合在一起的例子。让我们现在开始做吧。

1
2
3
4
5
6
7
8
function* someWords(){
  yield "hello";
  yield "world";
}

for (var word of someWords()) {
  alert(word);
}

这脚本够直接的了,但是,如果你注意到代码之间发生的事情,它就像是所有不同代码组成的一台戏,这时就是个不同的脚本了,它可能看起来会是这样:

(略)

这并不是什么哈姆雷特,但你可以想象一下这场景。

就如你在这戏中看到的,当一个生成器对象第一次出现时,它会暂停。每次当方法.next()被调用时,它就会醒来。

这动作是同步的、单线程的。注意,这里只有一个角色是可以在任意时间做任何事情,这角色不会互相打断对方,也不会与他人进行交互。他们轮流说话,而且,只要他们想,他们就可以想说多久就多久。(就像是莎士比亚!)

在这戏剧的一些版本中,生成器每次相当于被喂食给for-of循环。在这过程中,会有.next()方法在按序地执行,但并不会出现在你的任何代码中。这里,我把它放到舞台中,但对你们和你们的程序来说,所有的这些是在后台发生的,因为生成器和for-of循环在设计时是一起工作的,他们通过这个迭代接口 实现的。

所以,总结这里的点:

  • 生成器对象是个听话的机器人,它可以产出值;
  • 每次这机器人的程序包括一个简单的代码块,生成器的主体函数会生成这代码块。

怎么关闭一个生成器

生成器有一些非常巧妙的特性,我并没有在第一部分进行说明:

  • generator.return()
  • generator.next()可选参数
  • generator.throw(error)
  • yield*

我跳过这些主要是因为没有理解为什么这些特性会存在。我们很难注意到这些特性,在你们的脑子里并没有直接的印象。但是,当我们细想我们的程序是怎么使用生成器时,我们就能看到原因。

这里有些你可能会碰到过:

1
2
3
4
5
6
7
8
9
10
function doThings() {
  setup();
  try {
    // ..do some things ...
  } finally {
    cleanup();
  }
}

doThings();

这个cleanup方法可能会关闭连接或者是文件,释放系统资源,或者只是更新DOM去关闭“处理中”的加载图标。我们希望无论我们的工作是否成功完成都能执行它,所以它会出现在finally代码块中。

那么在生成器中会是怎么样的呢?

1
2
3
4
5
6
7
8
9
10
11
12
function* produceValues() {
  setup();
  try {
    // ... yield some values ...
  } finally {
    cleanup();
  }
}

for ( var value of produceValues()) {
  work(value);
}

这看起来很好,但是这里有点小问题:work(value)的调用并不在try的代码块中,如果有异常抛出,我们 cleanup 的时候会发生什么呢?

或者,当 for-of 循环中包含有 break 或者 return 语句时,cleanup的时候又会发生什么呢?

无论它怎么执行,你都可以控制它。

当我们谈到迭代器和for-of循环 时,我们说到迭代器的接口里有一个可选的 .return()的方法,当每次迭代完成之前它都会被自动调用。生成器也支持些方法。调用myGenerator.return()方法,会最终运行任意的finally的代码块,然后退出,就像是当前yield当作为return的返回语句一样。

注意,.return()并不会被JS的所有内容所调用,只有当JS使用到了迭代器的协议时才会。所以,一个生成器在没有运行 finally 代码块时有可能会被垃圾回收掉。

为什么这特性没有在剧本中体现呢?当一个任务正在依赖其它步骤完成的时候,生成器会被冻结,就像建造一幢大楼一样。突然,某人抛出了一个错误!for循环会抓取到它,并把它放到一边,并告诉生成器调用 .return()方法。生成器会安静地拆除整个建筑架子并关闭它。然后,for循环会获取到错误,一个普通的异常会继续处理中。

生成器管理

目前,我们看到了生成器与其用户之间的会话的其中一方面,先打断一下之前的剧场类比:

img

用户是主管,生成器会按照要求工作。但是,这并不是生成器的唯一编程方式。

在第一部分的文章中,我说过,生成器可以用于异步编程,也就是你可以用生成器来替代 异步的返回函数或者 promise 链。你可能会对它如何支持工作感到迷惑,为什么yield的能力那么有效(毕竟这是生成器特殊的能力)?毕竟,异步代码不仅是yield而已,它会触发更多的事情。然后,它返回到循环的事件中,等待处理那些异步进程,并完成它们。生成器是怎么做到这些的?没有返回函数,当数据 从文件、数据库和服务器中 返回时,生成器是怎么接收的呢?

为了接近答案并开始工作,如果我们通过仅能调用的.next()方法传递参数来给生成器,通过这交易,我们可以看到整个新类型的会话:

img

生成器的.next()方法实际上可以接收一个可选的参数,同时,它会聪明地将这参数作为value值返回到生成器中的yield的表达式中。是的,yield 并不类似于 return,这包含于个value值,生成器会获取一次。

1
var results = yield getDataAndLatte(request.areaCode);

上面一行简单的代码做了很多的事情:

  • 它会调用 getDataAndLatter(),此函数会返回一个我们在屏幕中看到的字符串“get me the database records for area code … “。
  • 它会暂停生成器,yield 这字符串值
  • 这时,许多时间会过去
  • 最终,某人调用.next({data: ..., coffee: ...})。我们存储相应的对象到本地的变量results中,并继续下一行代码。

为了展示其内容,下面的代码显示了的整个会话:

1
2
3
4
5
6
function* handle(request){
  var results = yield getDataAndLatter(request.areaCode);
  results.coffee.drink();
  var target = mostUrgentRecord(results.data);
  yield updateStatus(target.id, "ready");
}

注意,yield 仍然是之前的意思:暂停生成器并传递个value值给调用者。但是,事情发生了变化。生成器希望从其调用者中得到具体的支持性的行为,这意味着其希望调用者扮演着一个管理助手。

普通的函数并不会这样。它们倾向于服务它们调用者的需求。但是,你可以为生成器指定会话的代码,从而加大生成器与其调用者之间的联系。

管理助手式的生成器运行会是什么样?它并不是个复杂的事情,它可能看起来如:

1
2
3
4
5
6
7
8
9
10
11
12
function runGeneratorOnce(g, result) {
  var status = g.next(result);
  if (status.done) {
    return; // phew!
  }

  // The generator has asked us to fetch something and
  // call it back when we're done.
  doAsynchronousWorkIncludingEspressMachineOperations(
    status.value,
    (error, nextResult) => runGeneratorOnece(g, nextResult));
}

为了保证运行,我们不得不创建一个生成器并运行一次,类似:

1
runGeneratorOnce(handle(request), undefined);

在五月份,我提到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
function* concat(iter1, iter2) {
  for (var value of iter1) {
    yield value;
  }
  for (var value of iter2) {
    yield value;
  }
}

ES6还支持缩写的方法:

1
2
3
4
function* concat(iter1, iter2) {
  yield* iter1;
  yield* iter2;
}

一个普通的yield表达式会 yield 一个简单值,一个yield*表达式会消费整个迭代器并yield所有的值。

这个语法还解决另外一个有趣的问题:在一个生成器内部应该怎么调用生成器的问题。在普通的函数中,我们可以从一个函数中抽取一部分的代码,并重构成为一个单独的函数,而不会修改其行为。很明显,我们也将会重构生成器,但是我们需要找出一个方法来调用分解出去的子程序,同时需要确认在每次原有yield值之前,处理好子程序的yield操作,尽管那子程序也会生产出值。yield*就是处理这情况的方式。

1
2
3
4
5
6
7
function* factoredOutChunkOfCode() { ... }

function* refactoredFunction() {
  ...
  yield* factoredOutChunkOfCode();
  ...
}

可以类比想象一下,一个管道程序将部分子任务授予给其它进行操作。当编写大型的基于生成器的项目时,需要保持代码的清晰和易管理,你可以看到这方法是十分重要的,就类似函数在同步代码中的重要一般。

退场

好了,这就是生成器。我希望你也能够和我一样喜欢它,很高兴回过头再次提到它。

下周,我们将讨论另外让人脑洞大开的特性,也是ES6中新的特性。这是新类型,它是微小又狡猾的,在你还没有在这里了解它时可能你已经使用到它了。请在下周加入我们,一起开始深入ES6代理

Comments