异步迭代生成器
回想一下回调方法:
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();
在yield foo(11,31) 中,首先调用foo(11,31),它没有返回值(即返回undefined),所以我们发出了一个调用来请求数据,但实际上之后做的是yield undefined。这没问题,因为这段代码当前并不依赖yield 出来的值来做任何事情。
这里并不是在消息传递的意义上使用yield,而只是将其用于流程控制实现暂停/ 阻塞。实际上,它还是会有消息传递,但只是生成器恢复运行之后的单向消息传递。
所以,生成器在yield 处暂停,本质上是在提出一个问题:“我应该返回什么值来赋给变量text ?”谁来回答这个问题呢?
看一下foo(..)。如果这个Ajax 请求成功,我们调用:
it.next( data );
这会用响应数据恢复生成器,意味着我们暂停的yield 表达式直接接收到了这个值。然后随着生成器代码继续运行,这个值被赋给局部变量text。
我们在生成器内部有了看似完全同步的代码(除了yield 关键字本身),但隐藏在背后的是,在foo(..) 内的运行可以完全异步。
这是巨大的改进!对于我们前面陈述的回调无法以顺序同步的、符合我们大脑思考模式的方式表达异步这个问题,这是一个近乎完美的解决方案。
同步错误处理
try {
var text = yield foo(11, 31);
console.log(text);
} catch (err) {
console.error(err);
}
我们已经看到yield 是如何让赋值语句暂停来等待foo(..) 完成,使得响应完成后可以被赋给text。精彩的部分在于yield 暂停也使得生成器能够捕获错误。通过这段前面列出的代码把错误抛出到生成器中。
生成器yield 暂停的特性意味着我们不仅能够从异步函数调用得到看似同步的返回值,还可以同步捕获来自这些异步函数调用的错误!
生成器+Promise
ES6 中最完美的世界就是生成器(看似同步的异步代码)和Promise(可信任可组合)的结合。
function foo(x, y) {
return request("http://some.url.1/?x=" + x + "&y=" + y);
}
foo(11, 31).then(function(text) {
console.log(text);
}, function(err) {
console.error(err);
});
而这里支持Promise 的foo(..) 在发出Ajax 调用之后返回了一个promise。这暗示我们可以通过foo(..) 构造一个promise,然后通过生成器把它yield 出来,然后迭代器控制代码就可以接收到这个promise 了。
获得Promise 和生成器最大效用的最自然的方法就是yield 出来一个Promise,然后通过这个Promise 来控制生成器的迭代器。
function foo(x, y) {
return request("http://some.url.1/?x=" + x + "&y=" + y);
}
function* main() {
try {
var text = yield foo(11, 31);
console.log(text);
} catch (err) {
console.error(err);
}
}
var it = main();
var p = it.next().value;
// 等待promise p决议
p.then(function(text) {
it.next(text);
}, function(err) {
it.throw(err);
});
支持Promise 的Generator Runner
定义一个独立工具,叫作run(..):
// 在此感谢Benjamin Gruenbaum (@benjamingr on GitHub)的巨大改进!
function run(gen) {
var args = [].slice.call(arguments, 1),
it;
// 在当前上下文中初始化生成器
it = gen.apply(this, args);
// 返回一个promise用于生成器完成
return Promise.resolve().then(function handleNext(value) {
// 对下一个yield出的值运行
var next = it.next(value);
return (function handleResult(next) {
// 生成器运行完毕了吗?
if (next.done) {
return next.value;
}
// 否则继续运行
else {
return Promise.resolve(next.value).then(
// 成功就恢复异步循环,把决议的值发回生成器
handleNext,
// 如果value是被拒绝的 promise,
// 就把错误传回生成器进行出错处理
function handleErr(err) {
return Promise.resolve(it.throw(err)).then(handleResult);
});
}
})(next);
});
}
诚如所见,你可能并不愿意编写这么复杂的工具,并且也会特别不希望为每个使用的生成器都重复这段代码。所以,一个工具或库中的辅助函数绝对是必要的。尽管如此,我还是建议你花费几分钟时间学习这段代码,以更好地理解生成器+Promise 协同运作模式。
如何在运行Ajax 的例子中使用run(..) 和*main() 呢?
function *main() {
// ..
}
run( main );
就是这样!这种运行run(..) 的方式,它会自动异步运行你传给它的生成器,直到结束!
生成器中的Promise 并发
想象这样一个场景:你需要从两个不同的来源获取数据,然后把响应组合在一起以形成第三个请求,最终把最后一条响应打印出来。
你的第一直觉可能类似如下:
function* foo() {
var r1 = yield request("http://some.url.1");
var r2 = yield request("http://some.url.2");
var r3 = yield request("http://some.url.3/?v=" + r1 + "," + r2);
console.log(r3);
}
// 使用前面定义的工具run(..)
run(foo);
这段代码可以工作,但是针对我们特定的场景而言,它并不是最优的。
因为请求r1 和r2 能够——出于性能考虑也应该——并发执行,但是在这段代码中,它们是依次执行的; 直到请求URL"http://some.url.1" 完成后才会通过Ajax获取URL"http://some.url.2"。这两个请求是相互独立的,所以性能更高的方案应该是让它们同时运行。
function* foo() {
// 让两个请求"并行"
var p1 = request("http://some.url.1");
var p2 = request("http://some.url.2");
// 等待两个promise都决议
var r1 = yield p1;
var r2 = yield p2;
var r3 = yield request("http://some.url.3/?v=" + r1 + "," + r2);
console.log(r3);
}
// 使用前面定义的工具run(..)
run(foo);
这种流程控制模型如果听起来有点熟悉的话,是因为这基本上和我们在第3 章中通过Promise.all([ .. ]) 工具实现的gate 模式相同。因此,也可以这样表达这种流程控制:
function* foo() {
// 让两个请求"并行",并等待两个promise都决议
var results = yield Promise.all([
request("http://some.url.1"),
request("http://some.url.2")
]);
var r1 = results[0];
var r2 = results[1];
var r3 = yield request("http://some.url.3/?v=" + r1 + "," + r2);
console.log(r3);
}
// 使用前面定义的工具run(..)
run(foo);
生成器委托
你可能会从一个生成器调用另一个生成器,使用辅助函数run(..),就像这样:
function* foo() {
var r2 = yield request("http://some.url.2");
var r3 = yield request("http://some.url.3/?v=" + r2);
return r3;
}
function* bar() {
var r1 = yield request("http://some.url.1");
// 通过 run(..) "委托"给*foo()
var r3 = yield run(foo);
console.log(r3);
}
run(bar);
我们再次通过run(..) 工具从bar() 内部运行foo()。这里我们利用了如下事实:我们前面定义的run(..) 返回一个promise,这个promise 在生成器运行结束时(或出错退出时)决议。因此,如果从一个run(..) 调用中yield 出来一个promise 到另一个run(..) 实例中,它会自动暂停bar(),直到foo() 结束。
但其实还有一个更好的方法可以实现从bar() 调用foo(),称为yield 委托。yield 委托的具体语法是:yield * __(注意多出来的*)。在我们弄清它在前面的例子中的使用之前,
先来看一个简单点的场景:
function* foo() {
console.log("*foo() starting");
yield 3;
yield 4;
console.log("*foo() finished");
}
function* bar() {
yield 1;
yield 2;
yield* foo(); // yield委托!
yield 5;
}
var it = bar();
it.next().value; // 1
it.next().value; // 2
it.next().value; // *foo()启动
// 3
it.next().value; // 4
it.next().value; // *foo()完成
// 5
首先,和我们以前看到的完全一样,调用foo() 创建一个迭代器。然后yield * 把迭代器实例控制(当前bar() 生成器的)委托给/ 转移到了这另一个foo() 迭代器。
所以,前面两个it.next() 调用控制的是bar()。但当我们发出第三个it.next() 调用时,foo() 现在启动了,我们现在控制的是foo() 而不是bar()。这也是为什么这被称为委托:bar() 把自己的迭代控制委托给了foo()。
一旦it 迭代器控制消耗了整个foo() 迭代器,it 就会自动转回控制bar()。
现在回到前面使用三个顺序Ajax 请求的例子:
function* foo() {
var r2 = yield request("http://some.url.2");
var r3 = yield request("http://some.url.3/?v=" + r2);
return r3;
}
function* bar() {
var r1 = yield request("http://some.url.1");
// 通过 yeild* "委托"给*foo()
var r3 = yield* foo();
console.log(r3);
}
run(bar);