若是你已经读过这个系列的前三篇文章,那么你确定对ES6 generators很是了解了。但愿你能从中有所收获并让generator发挥它真正的做用。最后咱们要探讨的这个主题可能会让你血脉喷张,让你绞尽脑汁(说实话,写这篇文章让我很费脑子)。花点时间看下文章中的这些例子,相信对你仍是颇有帮助的。在学习上的投资会让你未来受益无穷。我彻底相信,在将来,JS中那些复杂的异步能力将起源于我这里的一些想法。html
首先,我写这一系列文章彻底是受Nolen @swannodette出色工做的启发。说真的,他写的全部文章都值得去读一读。我这里有一些连接能够分享给你:node
好了,让咱们正式开始对这个主题的探讨。我不是一个从具备Clojure(Clojure是一种运行在Java平台上的 Lisp 方言)背景转投到JS阵营的程序员,并且我也没有任何Go或者ClojureScript的经验。我发现本身在读这些文章的时候很快就会失去兴趣,所以我不得不作不少的实验并从中了解到一些有用的东西。git
在这个过程当中,我以为我已经有了一些相同的思想,并追求一样的目标,而这些都源自于一个不那么古板的思惟方式。程序员
我尝试建立了一个更简单的Go风格的CSP(以及ClojureScript core.async)APIs,同时我但愿能保留大部分的底层功能。也许有大神会看到我文章中遗漏的地方,这彻底有可能。若是真是这样的话,我但愿个人探索可以获得进一步的发展和演变,而我也将和你们一块儿来分享这个过程!es6
到底什么是CSP?说它是"communicating","Sequential","processes"究竟是什么意思呢?github
首先,CSP一词源自于Tony Hoare所著的“Communicating Sequential Processes”一书。里面全是有关CS的理论,若是你对学术方面的东西感兴趣的话,这本书绝对值得一读。我决不打算以一种让人难以理解的,深奥的,计算机科学的方式来阐述这个主题,而是会以一种轻松的非正式的方式来进行。编程
那咱们就从"Sequential"开始吧!这部分你应该已经很熟悉了。这是另一种谈论有关单线程和ES6 generators异步风格代码的方式。咱们来回忆一下generators的语法:数组
function *main() { var x = yield 1; var y = yield x; var z = yield (y * 2); }
上面代码中的每一条语句都会按顺序一个一个地执行。Yield关键字标明了代码中被阻塞的点(只能被generator函数本身阻塞,外部代码不能阻塞generator函数的执行),可是不会改变*main()函数中代码的执行顺序。这段代码很简单!promise
接下来咱们来讨论一下"processes"。这个是什么呢?数据结构
基本上,generator函数有点像一个虚拟的"process",它是咱们程序的一个独立的部分,若是JavaScript容许,它彻底能够与程序的其它部分并行执行。这听起来彷佛有点儿荒唐!若是generator函数访问共享内存(即,若是它访问除了本身内部定义的局部变量以外的“自由变量”),那么它就不是一个独立的部分。如今咱们假设有一个不访问外部变量的generator函数(在FP(Functional Programming函数式编程)的理论中咱们将它称之为一个"combinator"),所以从理论上来讲它能够在本身的process中运行,或者说做为本身的process来运行。
可是咱们说的是"processes",注意这个单词用的是复数,这是由于会存在两个或多个process在同一时间运行。换句话说,两个或多个generators函数会被放到一块儿来协同工做,一般是为了完成一项较大的任务。
为何要用多个单独的generator函数,而不是把它们都放到一个generator函数里呢?一个最重要的缘由就是:功能和关注点的分离。对于一个任务XYZ来讲,若是你将它分解成子任务X,Y和Z,那么在每一个子任务本身的generator函数中来实现功能将会使代码更容易理解和维护。这和将函数XYZ()拆分红X(),Y(),和Z(),而后在X()中调用Y(),在Y()中调用Z()是同样的道理。咱们将函数分解成一个个独立的子函数,下降代码的耦合度,从而使程序更加容易维护。
这就要说到"communicating"了。这个又是什么呢?就是合做。若是咱们将多个generators函数放在一些协同工做,它们彼此之间须要一个通讯信道(不只仅是访问共享的做用域,而是一个真正的能够被它们访问的独占式共享通讯信道)。这个通讯信道是什么呢?无论你发送什么内容(数字,字符串等),事实上你都不须要经过信道发送消息来进行通讯。通讯会像合做那样简单,就像将程序的控制权从一个地方转移到另一个地方。
为何须要转移控制?这主要是由于JS是单线程的,意思是说在任意给定的一个时间片断内只会有一个程序在运行,而其它程序都处在暂停状态。也就是说其它程序都处在它们各自任务的中间状态,不过只是被暂停执行,必要时会恢复并继续运行。
任意独立的"processes"之间能够神奇地进行通讯和合做,这听起来有点不靠谱。这种解耦的想法是好的,可是有点不切实际。相反,彷佛任何一个成功的CSP的实现都是对那些问题领域中已存在的、众所周知的逻辑集的有意分解,其中每一个部分都被特殊设计过从而使得各部分之间都能良好工做。
或许个人理解彻底是错的,可是我尚未看到任何一个切实可行的方法,可以让两个随机给定的generator函数能够以某种方式轻易地聚合在一块儿造成CSP对。它们都须要被设计成可以与其它部分一块儿工做,须要遵守彼此间的通讯协议等等。
在将CSP的理论应用到JS中,有一些很是有趣的探索。前面提到的David Nolen,他有几个颇有趣的项目,包括Om,以及core.async。Koa库(node.js)主要经过它的use(..)方法体现了这一点。而另一个对core.async/Go CSP API十分忠实的库是js-csp。
你确实应该去看看这些伟大的项目,看看其中的各类方法和例子,了解它们是如何在JS中实现CSP的。
由于我一直在努力探索将并行的CSP模式应用到我本身的JS代码中,因此对于使用CSP来扩展我本身的异步流程控制库asynquence来讲就是一件瓜熟蒂落的事。我写过的runner(..)插件(看上一篇文章:ES6 Generators的异步应用)就是用来处理generators函数的异步运行的,我发现它能够很容易被扩展用来处理多generators函数在同一时间运行,就像CSP的方式那样。
我要解决的第一个设计问题是:如何才能知道哪一个generator函数将得到下一个控制权?
要解决各个generators函数之间的消息或控制权的传递,每一个generator函数都必须拥有一个能让其它generators函数知道的ID,这看起来彷佛过于笨拙。通过各类尝试,我设定了一个简单的循环调度方法。若是你匹配了三个generators函数A,B和C,那么A将先得到控制权,当A yield时B将接管A的控制权,而后当B yield时C将接管B,而后又是A,以此类推。
可是如何才能实际转移generator函数的控制权呢?应该有一个显式的API吗?我再次进行了各类尝试,而后设定了一个更加隐式的方法,看起来和Koa有点相似(彻底是之外):每一个generator函数都得到一个共享"token"的引用,当yield时就表示要将控制权进行转移。
另外一个问题是消息通道应该长什么样。一种是很是正式的通讯API如core.async和js-csp(put(..)和take(..))。可是在我通过各类尝试以后,我比较倾向于另外一种不太正式的方法(甚至都谈不上API,而只是一个共享的数据结构,例如数组),它看起来彷佛是比较靠谱的。
我决定使用数组(称之为消息),你能够根据须要决定如何填充和清空数组的内容。你能够push()消息到数组中,从数组中pop()消息,按照约定将不一样的消息存放到数组中特定的位置,并在这些位置存放更复杂的数据结构等。
个人疑惑是有些任务须要传递简单的消息,而有些则须要传递复杂的消息,所以不要在一些简单的状况下强制这种复杂度,我选择不拘泥于消息通道的形式而使用数组(除数组自己外这里没有任何API)。在某些状况下它很容易在额外的形式上对消息传递机制进行分层,这对咱们来讲颇有用(参见下面的状态机示例)。
最终,我发现这些generator "processes"仍然得益于那些独立的generators可使用的异步功能。也就是说,若是不yield控制token,而yield一个Promise(或者一个异步队列),则runner(..)的确会暂停以等待返回值,但不会转移控制权,它会将结果返回给当前的process(generator)而保留控制权。
最后一点也许是最有争议或与本文中其它库差异最大的(若是我解释正确的话)。也许真正的CSP对这些方法不屑一顾,可是我发现个人选择仍是颇有用的。
好了,理论的东西讲得差很少了。咱们来看看具体的代码:
// 注意:为了简洁,省略了虚构的`multBy20(..)`和`addTo2(..)`异步数学函数 function *foo(token) { // 从通道的顶部获取消息 var value = token.messages.pop(); // 2 // 将另外一个消息存入通道 // `multBy20(..)`是一个promise-generating函数,它会延迟返回给定值乘以`20`的计算结果 token.messages.push( yield multBy20( value ) ); // 转移控制权 yield token; // 从CSP运行中的最后的消息 yield "meaning of life: " + token.messages[0]; } function *bar(token) { // 从通道的顶部获取消息 var value = token.messages.pop(); // 40 // 将另外一个消息存入通道 // `addTo2(..)` 是一个promise-generating函数,它会延迟返回给定值加上`2`的计算结果 token.messages.push( yield addTo2( value ) ); // 转移控制权 yield token; }
上面的代码中有两个generator "processes",*foo()和*bar()。它们都接收并处理一个令牌(固然,若是你愿意你能够随意叫什么都行)。令牌上的属性messages就是咱们的共享消息通道,当CSP运行时它会获取初始化传入的消息值进行填充(后面会讲到)。
yield token显式地将控制权转移到“下一个”generator函数(循环顺序)。可是,yield multBy20(value)和yield addTo2(value)都是yield一个promises(从这两个虚构的延迟计算函数中返回的),这表示generator函数此时是处于暂停状态直到promise完成。一旦promise完成,当前处于控制中的generator函数会恢复并继续运行。
不管最终yield会返回什么,上面的例子中yield返回的是一个表达式,都表示咱们的CSP运行完成的消息(见下文)。
如今咱们有两个CSP process generators,咱们来看看如何运行它们?使用asynquence:
// 开始一个sequence,初始message的值是2 ASQ( 2 ) // 将两个CSP processes进行配对一块儿运行 .runner( foo, bar ) // 不管接收到的message是什么,都将它传入sequence中的下一步 .val( function(msg){ console.log( msg ); // 最终返回42 } );
这只是一个很简单的例子,但我以为它能很好地用来解释上面的这些概念。你能够尝试一下(试着改变一些值),这有助于你理解这些概念并本身动手编写代码!
让咱们来看一个经典的CSP例子,但只是从咱们目前已有的一些简单的发现开始,而不是从咱们一般所说的纯粹学术的角度来展开讨论。
Ping-pong。一个颇有趣的游戏,对吗?也是我最喜欢的运动。
让咱们来想象一下你已经完成了这个乒乓球游戏的代码,你经过一个循环来运行游戏,而后有两部分代码(例如在if或switch语句中的分支),每一部分表明一个对应的玩家。代码运行正常,你的游戏运行起来就像是一个乒乓球冠军!
可是按照咱们上面讨论过的,CSP在这里起到了什么样的做用呢?就是功能和关注点的分离。那么具体到咱们的乒乓球游戏中,这个分离指的就是两个不一样的玩家!
那么,咱们能够在一个很是高的层面上用两个"processes"(generators)来模拟咱们的游戏,每一个玩家一个"process"。当咱们实现代码细节的时候,咱们会发如今两个玩家之家存在控制的切换,咱们称之为"glue code"(胶水代码(译:在计算机编程领域,胶水代码也叫粘合代码,用途是粘合那些可能不兼容的代码。可使用与胶合在一块儿的代码相同的语言编写,也能够用单独的胶水语言编写。胶水代码不实现程序要求的任何功能,它一般出如今代码中,使现有的库或者程序在外部函数接口(如Java本地接口)中进行互操做。胶水代码在快速原型开发环境中很是高效,可让几个组件被快速集成到单个语言或者框架中。)),这个任务自己可能须要第三个generator的代码,咱们能够将它模拟成游戏的裁判。
咱们打算跳过各类特定领域的问题,如计分、游戏机制、物理原理、游戏策略、人工智能、操做控制等。这里咱们惟一须要关心的部分就是模拟打乒乓球的往复过程(这实际上也表明了咱们CSP的控制转移)。
想看demo的话能够在这里运行(注意:在支持ES6 JavaScript的最新版的FireFox nightly或Chrome中查看generators是如何工做的)。如今,让咱们一块儿来看看代码。首先,来看看asynquence sequence长什么样?
ASQ( ["ping","pong"], // 玩家姓名 { hits: 0 } // 球 ) .runner( referee, player, player ) .val( function(msg){ message( "referee", msg );
咱们初始化了一个messages sequence:["ping", "pong"]和{hits: 0}。一下子会用到。而后,咱们设置了一个包含3个processes运行的CSP(相互协同工做):一个*referee()和两个*player()实例。在游戏结束时最终的message会被传递给sequence中的下一步,做为referee的输出message。下面是referee的实现代码:
function *referee(table){ var alarm = false; // referee经过秒表(10秒)为游戏设置了一个计时器 setTimeout( function(){ alarm = true; }, 10000 ); // 当计时器警报响起时游戏中止 while (!alarm) { // 玩家继续游戏 yield table; } // 通知玩家游戏已结束 table.messages[2] = "CLOSED"; // 裁判宣布时间到了 yield "Time's up!"; } } );
这里咱们用table来模拟控制令牌以解决咱们上面说的那些特定领域的问题,这样就能很好地来描述当一个玩家将球打回去的时候控制权被yield给另外一个玩家。*referee()中的while循环表示只要秒表没有停,程序就会一直yield table(将控制权转移给另外一个玩家)。当计时器结束时退出while循环,referee将会接管控制权并宣布"Time's up!"游戏结束了。
再来看看*player() generator的实现代码(咱们使用两个实例):
function *player(table) { var name = table.messages[0].shift(); var ball = table.messages[1]; while (table.messages[2] !== "CLOSED") { // 击球 ball.hits++; message( name, ball.hits ); // 模拟将球打回给另外一个玩家中间的延迟 yield ASQ.after( 500 ); // 游戏继续? if (table.messages[2] !== "CLOSED") { // 球如今回到另外一个玩家那里 yield table; } } message( name, "Game over!" ); }
第一个玩家将他的名字从message数组的第一个元素中移除("ping"),而后第二个玩家取他的名字("pong"),以便他们都能正确地识别本身(译:注意这里是两个*player()的实例,在两个不一样的实例中,经过table.messages[0].shift()能够获取各自不一样的玩家名字)。同时两个玩家都保持对共享球的引用(使用hits计数器)。
当玩家尚未听到裁判说结束,就“击球”并累加计数器(并输出一个message来通知它),而后等待500毫秒(假设球以光速运行不占用任什么时候间)。若是游戏还在继续,他们就yield table到另外一个玩家那里。就是这样。
在这里能够查看完整代码,从而了解代码的各部分是如何工做的。
最后一个例子:将一个状态机定义为由一个简单的helper驱动的一组generator协同程序。Demo(注意:在支持ES6 JavaScript的最新版的FireFox nightly或Chrome中查看generators是如何工做的)。
首先,咱们定义一个helper来控制有限的状态处理程序。
function state(val,handler) { // 管理状态的协同处理程序(包装器) return function*(token) { // 状态转换处理程序 function transition(to) { token.messages[0] = to; } // 默认初始状态(若是尚未设置) if (token.messages.length < 1) { token.messages[0] = val; } // 继续运行直到最终的状态为true while (token.messages[0] !== false) { // 判断当前状态是否和处理程序匹配 if (token.messages[0] === val) { // 委托给状态处理程序 yield *handler( transition ); } // 将控制权转移给另外一个状态处理程序 if (token.messages[0] !== false) { yield token; } } }; }
state(..) helper为特定的状态值建立了一个delegating-generator包装器,这个包装器会自动运行状态机,并在每一个状态切换时转移控制权。
依照惯例,我决定使用共享token.messages[0]的位置来保存咱们状态机的当前状态。这意味着你能够经过从序列中前一步传入的message来设定初始状态。可是若是没有传入初始值的话,咱们会简单地将第一个状态做为默认的初始值。一样,依照惯例,最终的状态会被假设为false。这很容易修改以适合你本身的须要。
状态值能够是任何你想要的值:numbers,strings等。只要该值能够被===运算符严格测试经过,你就可使用它做为你的状态。
在下面的示例中,我展现了一个状态机,它能够按照特定的顺序在四个数值状态间进行转换:1->4->3->2。为了演示,这里使用了一个计数器,所以能够实现屡次循环转换。当咱们的generator状态机到达最终状态时(false),asynquence序列就会像你所指望的那样移动到下一步。
// 计数器(仅用做演示) var counter = 0; ASQ( /* 可选:初始状态值 */ ) // 运行状态机,转换顺序:1 -> 4 -> 3 -> 2 .runner( // 状态`1`处理程序 state( 1, function*(transition){ console.log( "in state 1" ); yield ASQ.after( 1000 ); // 暂停1s yield transition( 4 ); // 跳到状态`4` } ), // 状态`2`处理程序 state( 2, function*(transition){ console.log( "in state 2" ); yield ASQ.after( 1000 ); // 暂停1s // 仅用做演示,在状态循环中保持运行 if (++counter < 2) { yield transition( 1 ); // 跳转到状态`1` } // 所有完成! else { yield "That's all folks!"; yield transition( false ); // 跳转到最终状态 } } ), // 状态`3`处理程序 state( 3, function*(transition){ console.log( "in state 3" ); yield ASQ.after( 1000 ); // 暂停1s yield transition( 2 ); // 跳转到状态`2` } ), // 状态`4`处理程序 state( 4, function*(transition){ console.log( "in state 4" ); yield ASQ.after( 1000 ); // 暂停1s yield transition( 3 ); // 跳转到状态`3` } ) ) // 状态机完成,移动到下一步 .val(function(msg){ console.log( msg ); });
应该很容易地跟踪上面的代码来查看到底发生了什么。yield ASQ.after(1000)显示了这些generators能够根据须要作任何类型的基于promise/sequence的异步工做,就像咱们在前面所看到的同样。yield transition(...)表示如何转换到一个新的状态。上面代码中的state(..) helper完成了处理yield* delegation和状态转换的主要工做,而后整个程序的主要流程看起来十分简单,表述也很清晰流畅。
CSP的关键是将两个或更多的generator "processes"链接在一块儿,给它们一个共享的通讯信道,以及一种能够在彼此间传输控制的方法。
JS中有不少的库都或多或少地采用了至关正式的方法来与Go和Clojure/ClojureScript APIs或语义相匹配。这些库的背后都有着很是棒的开发者,对于进一步探索CSP来讲他们都是很是好的资源。
asynquence试图采用一种不太正式而又但愿仍然可以保留主要结构的方法。若是没有别的 ,asynquence的runner(..) 能够做为你实验和学习CSP-like generators的入门。
最好的部分是asynquence CSP与其它异步功能(promises,generators,流程控制等)在一块儿工做。如此一来,你即可以掌控一切,使用任何你手头上合适的工具来完成任务,而全部的这一切都只在一个小小的lib中。
如今咱们已经在这四篇文章中详细探讨了generators,我但愿你可以从中受益并得到灵感以探索如何革新本身的异步JS代码!你将用generators来创造什么呢?