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 请求,等它完成之后打印出响应结果。”并且,当然,我们 只在这个流程控制中表达了两个步骤,而这种表达能力是可以无限扩展的,以便我们无论 需要多少步骤都可以表达。