(1) 对迭代器的 next() 的第一个调用会把生成器从未初始化状态转移到状态 1,然后调用 process() 来处理这个状态。request(…) 的返回值是对应 Ajax 响应的 promise,作为 value 属性从 next() 调用返回。
(2) 如果 Ajax 请求成功,第二个 next(…) 调用应该发送 Ajax 响应值进来,这会把状态转 移到状态 2。再次调用 process(…)(这次包括传入的 Ajax 响应值),从 next(…) 返回 的 value 属性将是 undefined。
(3) 然而,如果 Ajax 请求失败的话,就会使用错误调用 throw(…),这会把状态从 1 转移到 3(而非 2)。再次调用 process(…),这一次包含错误值。这个 case 返回 false,被作 为 throw(…) 调用返回的 value 属性。
从外部来看(也就是说,只与迭代器交互),这个普通函数 foo(…) 与生成器 *foo(…) 的 工作几乎完全一样。所以我们已经成功地把 ES6 生成器转为了前 ES6 兼容代码!
然后就可以手工实例化生成器并控制它的迭代器了,调用var it = foo(“…”)和 it.next(…) 等。甚至更好的是,我们可以把它传给前面定义的工具 run(…),就像 run(foo,“…”)。
自动转换
前面的 ES6 生成器到前 ES6 等价代码的手工推导练习,向我们教授了概念上生成器是如何 工作的。但是,这个变换非常复杂,并且对于代码中的其他生成器而言也是不可移植的。 这部分工作通过手工实现十分不实际,会完全抵消生成器的一切优势。
但幸运的是,已经有一些工具可以自动把 ES6 生成器转化为我们推导出来的 结果那样的代码。它们不仅会为我们完成这些笨重的工作,还会处理我们忽略的几个枝节 问题。
如果使用 regenerator 来转换前面的生成器的话,以下是产生的代码(本书写作之时): ```js
// request(…)是一个支持Promise的Ajax工具
var foo = regeneratorRuntime.mark(function foo(url) {
var val;
return regeneratorRuntime.wrap(function foo$(context$1$0) {
while (1) switch (context$1$0.prev = context$1$0.next) {
case 0:
context$1$0.prev = 0;
console.log( “requesting:”, url );
context$1$0.next = 4;
return request( url );
case 4:
val = context$1$0.sent;
console.log( val );
context$1$0.next = 12;
break;
case 8:
context$1$0.prev = 8;
context$1$0.t0 = context$1$0.catch(0);
console.log(“Oops:”, context$1$0.t0);
return context$1$0.abrupt(“return”, false);
case 12:
case “end”:
return context$1$0.stop();
}
}, foo, this, [[0, 8]]);
});
这与我们手工推导的结果有一些明显的相似之处,比如那些 switch/case 语句,而且我们 甚至看到了移出闭包的 val,就像我们做的一样。
当然,一个不同之处是,regenerator 的变换需要一个辅助库 regeneratorRuntime,其中包 含了管理通用生成器和迭代器的所有可复用逻辑。这些重复代码中有很多和我们的版本不 同,但即使这样,很多概念还是可以看到的,比如context$1$0.next = 记录生成器的下 一个状态。
主要的收获是,生成器不再局限于只能在 ES6+ 环境中使用。一旦理解了这些概念,就可 以在代码中使用,然后使用工具将其变换为与旧环境兼容的代码。
这比仅仅将修改后的 Promise API 用作前 ES6 Promise 所做的工作要多得多,但是,付出的 代价是值得的,因为在实现以合理的、明智的、看似同步的、顺序的方式表达异步流程方 面,生成器的优势太多了。
一旦迷上了生成器,就再也不会想回到那一团乱麻的异步回调地狱中了。