异步地迭代Generator

generator要怎样处理异步编码模式,解决回调和类似的问题?让我们开始回答这个重要的问题。

我们应当重温一下第三章的一个场景。回想一下这个回调方式:

  1. function foo(x,y,cb) {
  2. ajax(
  3. "http://some.url.1/?x=" + x + "&y=" + y,
  4. cb
  5. );
  6. }
  7. foo( 11, 31, function(err,text) {
  8. if (err) {
  9. console.error( err );
  10. }
  11. else {
  12. console.log( text );
  13. }
  14. } );

如果我们想用generator表示相同的任务流控制,我们可以:

  1. function foo(x,y) {
  2. ajax(
  3. "http://some.url.1/?x=" + x + "&y=" + y,
  4. function(err,data){
  5. if (err) {
  6. // 向`*main()`中扔进一个错误
  7. it.throw( err );
  8. }
  9. else {
  10. // 使用收到的`data`来继续`*main()`
  11. it.next( data );
  12. }
  13. }
  14. );
  15. }
  16. function *main() {
  17. try {
  18. var text = yield foo( 11, 31 );
  19. console.log( text );
  20. }
  21. catch (err) {
  22. console.error( err );
  23. }
  24. }
  25. var it = main();
  26. // 使一切开始运行!
  27. it.next();

一眼看上去,这个代码段要比以前的回调代码更长,而且也许看起来更复杂。但不要让这种印象误导你。generator的代码段实际上要好 太多 了!但是这里有很多我们需要讲解的。

首先,让我们看看代码的这一部分,也是最重要的部分:

  1. var text = yield foo( 11, 31 );
  2. console.log( text );

花一点时间考虑一下这段代码如何工作。我们调用了一个普通的函数foo(..),而且我们显然可以从Ajax调用那里得到text,即便它是异步的。

这怎么可能?如果你回忆一下第一章的最开始,我们有一个几乎完全一样的代码:

  1. var data = ajax( "..url 1.." );
  2. console.log( data );

但是这段代码不好用!你能发现不同吗?它就是在generator中使用的yield

这就是魔法发生的地方!是它允许我们拥有一个看起来是阻塞的,同步的,但实际上不会阻塞整个程序的代码;它仅仅暂停/阻塞在generator本身的代码。

yield foo(11,31)中,首先foo(11,31)调用被发起,它什么也不返回(也就是undefined),所以我们发起了数据请求,然后我们实际上做的是yield undefined。这没问题,因为这段代码现在没有依赖yield的值来做任何有趣的事。我们在本章稍后再重新讨论这个问题。

在这里,我们没有将yield作为消息传递的工具,只是作为进行暂停/阻塞的流程控制的工具。实际上,它会传递消息,但是只是单向的,在generator被继续运行之后。

那么,generator暂停在了yield,它实质上再问一个问题,“我该将什么值返回并赋给变量text?”谁来回答这个问题?

看一下foo(..)。如果Ajax请求成功,我们调用:

  1. it.next( data );

这将使generator使用应答数据继续运行,这意味着我们暂停的yield表达式直接收到这个值,然后因为它重新开始以运行generator代码,所以这个值被赋给本地变量text

很酷吧?

退一步考虑一下它的意义。我们在generator内部的代码看起来完全是同步的(除了yield关键字本身),但隐藏在幕后的是,在foo(..)内部,操作可以完全是异步的。

这很伟大! 这几乎完美地解决了我们前面遇到的问题:回调不能像我们的大脑可以关联的那样,以一种顺序,同步的风格表达异步处理。

实质上,我们将异步处理作为实现细节抽象出去,以至于我们可以同步地/顺序地推理我们的流程控制:“发起Ajax请求,然后在它完成之后打印应答。” 当然,我们仅仅在这个流程控制中表达了两个步骤,但同样的能力可以无边界地延伸,让我们需要表达多少步骤,就表达多少。

提示: 这是一个如此重要的认识,为了充分理解,现在回过头去再把最后三段读一遍!

同步错误处理

但是前面的generator代码会 出更多的好处给我们。让我们把注意力移到generator内部的try..catch上:

  1. try {
  2. var text = yield foo( 11, 31 );
  3. console.log( text );
  4. }
  5. catch (err) {
  6. console.error( err );
  7. }

这是怎么工作的?foo(..)调用是异步完成的,try..catch不是无法捕捉异步错误吗?就像我们在第三章中看到的?

我们已经看到了yield如何让赋值语句暂停,来等待foo(..)去完成,以至于完成的响应可以被赋予text。牛X的是,yield暂停 允许generator来catch一个错误。我们在前面的例子,我们用这一部分代码将这个错误抛出到generator中:

  1. if (err) {
  2. // 向`*main()`中扔进一个错误
  3. it.throw( err );
  4. }

generator的yield暂停特性不仅意味着我们可以从异步的函数调用那里得到看起来同步的return值,还意味着我们可以同步地捕获这些异步函数调用的错误!

那么我们看到了,我们可以将错误 抛入 generator,但是将错误 抛出 一个generator呢?和你期望的一样:

  1. function *main() {
  2. var x = yield "Hello World";
  3. yield x.toLowerCase(); // 引发一个异常!
  4. }
  5. var it = main();
  6. it.next().value; // Hello World
  7. try {
  8. it.next( 42 );
  9. }
  10. catch (err) {
  11. console.error( err ); // TypeError
  12. }

当然,我们本可以用throw ..手动地抛出一个错误,而不是制造一个异常。

我们甚至可以catch我们throw(..)进generator的同一个错误,实质上给了generator一个机会来处理它,但如果generator没处理,那么 迭代器 代码必须处理它:

  1. function *main() {
  2. var x = yield "Hello World";
  3. // 永远不会跑到这里
  4. console.log( x );
  5. }
  6. var it = main();
  7. it.next();
  8. try {
  9. // `*main()`会处理这个错误吗?我们走着瞧!
  10. it.throw( "Oops" );
  11. }
  12. catch (err) {
  13. // 不,它没处理!
  14. console.error( err ); // Oops
  15. }

使用异步代码的,看似同步的错误处理(通过try..catch)在可读性和可推理性上大获全胜。