4.3 异步迭代生成器
生成器与异步编码模式及解决回调问题等,有什么关系呢?让我们来回答这个重要的问题。
我们应该重新讨论第 3 章中的一个场景。回想一下回调方法:
function foo(x,y,cb) { ajax( "http://some.url.1/?x=" + x + "&y=" + y, cb ); } foo( 11, 31, function(err,text) { if (err) { console.error( err ); } else { console.log( text ); } } );
如果想要通过生成器来表达同样的任务流程控制,可以这样实现:
function foo(x,y) { ajax( "http://some.url.1/?x=" + x + "&y=" + y, function(err,data){ if (err) { // 向*main()抛出一个错误 it.throw( err ); } else { // 用收到的data恢复*main() it.next( data ); } } ); } function *main() { try { var text = yield foo( 11, 31 ); console.log( text ); } catch (err) { console.error( err ); } } var it = main(); // 这里启动! it.next();
第一眼看上去,与之前的回调代码对比起来,这段代码更长一些,可能也更复杂一些。但是,不要被表面现象欺骗了!生成器代码实际上要好得多!不过要解释这一点还是比较复杂的。
首先,让我们查看一下最重要的这段代码:
var text = yield foo( 11, 31 ); console.log( text );
请先花点时间思考一下这段代码是如何工作的。我们调用了一个普通函数 foo(..) ,而且显然能够从 Ajax 调用中得到 text ,即使它是异步的。
这怎么可能呢?如果你回想一下第 1 章的开始部分的话,我们给出了几乎相同的代码:
var data = ajax( "..url 1.." ); console.log( data );
但是,这段代码不能工作!你能指出其中的区别吗?区别就在于生成器中使用的 yield 。
这就是奥秘所在!正是这一点使得我们看似阻塞同步的代码,实际上并不会阻塞整个程序,它只是暂停或阻塞了生成器本身的代码。
在 yield foo(11,31) 中,首先调用 foo(11,31) ,它没有返回值(即返回 undefined ),所以我们发出了一个调用来请求数据,但实际上之后做的是 yield undefined 。这没问题,因为这段代码当前并不依赖 yield 出来的值来做任何事情。本章后面会再次讨论这一点。
这里并不是在消息传递的意义上使用 yield ,而只是将其用于流程控制实现暂停 / 阻塞。实际上,它还是会有消息传递,但只是生成器恢复运行之后的单向消息传递。
所以,生成器在 yield 处暂停,本质上是在提出一个问题:“我应该返回什么值来赋给变量 text ?”谁来回答这个问题呢?
看一下 foo(..) 。如果这个 Ajax 请求成功,我们调用:
it.next( data );
这会用响应数据恢复生成器,意味着我们暂停的 yield 表达式直接接收到了这个值。然后随着生成器代码继续运行,这个值被赋给局部变量 text 。
很酷吧?
回头往前看一步,思考一下这意味着什么。我们在生成器内部有了看似完全同步的代码(除了 yield 关键字本身),但隐藏在背后的是,在 foo(..) 内的运行可以完全异步。
这是巨大的改进!对于我们前面陈述的回调无法以顺序同步的、符合我们大脑思考模式的方式表达异步这个问题,这是一个近乎完美的解决方案。
从本质上而言,我们把异步作为实现细节抽象了出去,使得我们可以以同步顺序的形式追踪流程控制:“发出一个 Ajax 请求,等它完成之后打印出响应结果。”并且,当然,我们只在这个流程控制中表达了两个步骤,而这种表达能力是可以无限扩展的,以便我们无论需要多少步骤都可以表达。
这是一个很重要的领悟,回过头去把上面三段重读一遍,让它融入你的思想吧!
同步错误处理
前面的生成器代码甚至还给我们带来了更多其他的好处。让我们把注意力转移到生成器内部的 try..catch :
try { var text = yield foo( 11, 31 ); console.log( text ); } catch (err) { console.error( err ); }
这是如何工作的呢?调用 foo(..) 是异步完成的,难道 try..catch 不是无法捕获异步错误,就像我们在第 3 章中看到的一样吗?
我们已经看到 yield 是如何让赋值语句暂停来等待 foo(..) 完成,使得响应完成后可以被赋给 text 。精彩的部分在于 yield 暂停也使得生成器能够捕获错误。通过这段前面列出的代码把错误抛出到生成器中:
if (err) { // 向*main()抛出一个错误 it.throw( err ); }
生成器 yield 暂停的特性意味着我们不仅能够从异步函数调用得到看似同步的返回值,还可以同步捕获来自这些异步函数调用的错误!
所以我们已经知道,我们可以把错误抛入生成器中,不过如果是从生成器向外抛出错误呢?正如你所料 :
function *main() { var x = yield "Hello World"; yield x.toLowerCase(); // 引发一个异常! } var it = main(); it.next().value; // Hello World try { it.next( 42 ); } catch (err) { console.error( err ); // TypeError }
当然,也可以通过 throw .. 手工抛出一个错误,而不是通过触发异常。
甚至可以捕获通过 throw(..) 抛入生成器的同一个错误,基本上也就是给生成器一个处理它的机会;如果没有处理的话,迭代器代码就必须处理:
function *main() { var x = yield "Hello World"; // 永远不会到达这里 console.log( x ); } var it = main(); it.next(); try { // *main()会处理这个错误吗?看看吧! it.throw( "Oops" ); } catch (err) { // 不行,没有处理! console.error( err ); // Oops }
在异步代码中实现看似同步的错误处理(通过 try..catch )在可读性和合理性方面都是一个巨大的进步。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论