B.3 生成器协程
希望第 4 章已经帮助你熟悉了 ES6 生成器。具体来说,我们想要再次讨论“生成器并发”,甚至更加深入。
设想一个工具 runAll(..) ,它能接受两个或更多的生成器,并且并发地执行它们,让它们依次进行合作式 yield 控制,并支持可选的消息传递。
除了可以运行单个生成器到结束之外,我们在附录 A 讨论的 ASQ#runner(..) 是 runAll(..) 概念的一个相似实现,后者可以并发运行多个生成器到结束。
因此,让我们来看看如何实现第 4 章中并发 Ajax 的场景:
ASQ( "http://some.url.2" ) .runner( function*(token){ // 传递控制 yield token; var url1 = token.messages[0]; // "http://some.url.1" // 清空消息,重新开始 token.messages = []; var p1 = request( url1 ); // 传递控制 yield token; token.messages.push( yield p1 ); }, function*(token){ var url2 = token.messages[0]; // "http://some.url.2" // 传递消息并传递控制 token.messages[0] = "http://some.url.1"; yield token; var p2 = request( url2 ); // 传递控制 yield token; token.messages.push( yield p2 ); // 把结果传给下一个序列步骤 return token.messages; } ) .val( function(res){ // res[0]来自"http://some.url.1" // res[1]来自"http://some.url.2" } );
ASQ#runner(..) 和 runAll(..) 之间的主要区别如下。
· 每个生成器(协程)都被提供了一个叫作 token 的参数。这是一个特殊的值,想要显式把控制传递到下一个协程的时候就 yield 这个值。
· token.messages 是一个数组,其中保存了从前面一个序列步骤传入的所有消息。它也是一个你可以用来在协程之间共享消息的数据结构。
· yield 一个 Promise(或序列)值不会传递控制,而是暂停这个协程处理,直到这个值准备好。
· 从协程处理运行最后 return 的或 yield 的值将会被传递到序列中的下一个步骤。
在基本的 ASQ#runner(..) 功能之上添加辅助函数用于不同的用途也是很容易实现的。
状态机
对许多程序员来说,一个可能很熟悉的例子就是状态机。在简单的装饰工具的帮助下,你可以创建一个很容易表达的状态机处理器。
让我们来设想这样一个工具。我们将其称为 state(..) ,并给它传入两个参数:一个状态值和一个处理这个状态的生成器。创建和返回要传递给 ASQ#runner(..) 的适配器生成器这样的苦活将由 state(..) 负责。
考虑:
function state(val,handler) { // 为这个状态构造一个协程处理函数 return function*(token) { // 状态转移处理函数 function transition(to) { token.messages[0] = to; } // 设定初始状态(如果还未设定的话) if (token.messages.length < 1) { token.messages[0] = val; } // 继续,直到到达最终状态(false) while (token.messages[0] !== false) { // 当前状态与这个处理函数匹配吗? if (token.messages[0] === val) { // 委托给状态处理函数 yield *handler( transition ); } // 还是把控制转移到另一个状态处理函数? if (token.messages[0] !== false) { yield token; } } }; }
如果仔细观察的话,可以看到 state(..) 返回了一个接受一个 token 的生成器,然后它建立了一个 while 循环,该循环将持续运行,直到状态机到达终止状态(这里我们随机设定为值 false )。这正是我们想要传给 ASQ#runner(..) 的那一类生成器!
我们还随意保留了 token.messages[0] 槽位作为放置状态机当前状态的位置,用于追踪,这意味着我们甚至可以把初始状态值作为种子从序列中的前一个步骤传入。
如何将辅助函数 state(..) 与 ASQ#runner(..) 配合使用呢?
var prevState; ASQ( /*可选:初始状态值 */ 2 ) // 运行状态机 // 转移: 2 -> 3 -> 1 -> 3 -> false .runner( // 状态1处理函数 state( 1, function *stateOne(transition){ console.log( "in state 1" ); prevState = 1; yield transition( 3 ); // 转移到状态3 } ), // 状态2处理函数 state( 2, function *stateTwo(transition){ console.log( "in state 2" ); prevState = 2; yield transition( 3 ); // 转移到状态3 } ), // 状态3处理函数 state( 3, function *stateThree(transition){ console.log( "in state 3" ); if (prevState === 2) { prevState = 3; yield transition( 1 ); // 转移到状态1 } // 完毕! else { yield "That's all folks!"; prevState = 3; yield transition( false ); // 最终状态 } } ) ) // 状态机完毕,继续 .val( function(msg){ console.log( msg ); // 就这些! } );
有很重要的一点需要指出,生成器 *stateOne(..) 、*stateTwo(..) 和 *stateThree(..) 三者本身在每次进入状态时都会被再次调用,而在你通过 transition(..) 转移到其他值时就会结束。尽管这里没有展示,但这些状态生成器处理函数显然可以通过 yield Promise/ 序列 /thunk 来异步暂停。
底层隐藏的由辅助函数 state(..) 产生并实际上传给 ASQ#runner(..) 的生成器是在整个状态机生存期都持续并发运行的,它们中的每一个都会把协作式 yield 控制传递到下一个,以此类推。
查看这个 ping pong 的例子(http://jsbin.com/qutabu/1/edit?js,output ),可以得到更多关于使用由 ASQ#runner(..) 驱动的生成器进行协作式并发的示例。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论