操作流是应对一个函数的执行依赖于多个异步操作的结果而产生的。这其实是事件派发的一种。用IE only的写法如下:
document.attachEvent("onclick",function(){ alert("fire click"); }); var e = document.createEventObject(); document.fireEvent("onclick",e);
用jQuery的写法如下:
$(document).click(function(){ alert("fire click"); }).trigger("click")
jQuery还能对应自定义事件,不过所有库的自定义事件实现也别无二致。但这种事件派发只能针对当前指定的某个事件,有时我想,这些自定义事件有没有必要存在。
比如一个游戏,要通过QTE来打BOSS,如S+D+F,用jQuery实现如下:
var keys = 0, keystateTime = 0; function checkInterval(){ if(!keystateTime){ keystateTime = new Date; }else{ var now = new Date - keystateTime; if(now > 500){ keystateTime = keys = 0; } } } $(document).bind("keypress",function(e){ if(e.which === 115 || e.which === 100 || e.which === 102){//SDF keys += e.which; checkInterval();//热键的每个操作之间间隔应该很小 dom.fire("qte") } }); $(document).bind("qte",function(e){ if(keys === 115+100+102 && keystateTime){ console.log("开始攻击") } });
我们不能说qte事件依赖于keypress事件,准确来说,是attack这步操作依赖于三次满足条件的按下操作。keypress绑定只是更前期的准备而已。真正相关联的是在回调与回调之间。qte的回调函数是上面那三个回调的回调。如果把事件函数的回调函数当成一次操作。那么它们四个就是一个操作流。hotKey在这过程中保是一个标识,用于移除与派发的标识。
好了,下面正式进入主题。
使用已有资源,对已有事、物进行加工改造,使之产生目标结果的过程叫操作。
操作分为两大类:同步操作与异步操作。同步操作,如setStyle,getAttribute, el.innerHTML= "XXX"就是同步的。异步操作见各种事件,setTimeout回调。
同步操作是即时的,阻塞的,必然会发生的。异步操作是延迟的,非阻塞的,不可预期的。
事件是指经过分类,拥有相同特性的导步操作的统称。这个类别名,就是它们的事件名,click, keypress, resize, scroll....
已存的事件系统都没有针对这种复杂的回调情况设置对应的API,不过也不奇怪,许多javascript框架都是由其他语言出身的高手的写的,他们的思维是过程式或OO式,而不是函数式。异步编程在其他语言也没有像JS那么突出,像浏览器需要处理资源的加载,因此自顶向下的同步编程风格必然被回调风格所代替。如果一个回调依赖于另一个回调的结果,那么我们就陷入回调套嵌的泥沼了。
因此要解决这问题,必须要有一个机制,能实现多路监听,收集过程中的每个回调的结果,触发最终回调。这个过程是不是与模块加载系统非常像。模块加载的过程,通过require方法请求多个依赖模块,将每个请求回来的模块的结果装配到框架中,待到所有依赖都存在于框加中时就执行最终回调。
下面是我实现操作流的一些API介绍
- 通过var flow = dom.flow(names,fn,reload)工厂方法生成一个操作流,有没有参数无所谓,反正也是调用原型上的bind方法。
- flow.bind(names,fn,reload),实现多路监听,names为一个用逗号或空格隔开的字符串,每个单词为要监听的操作, fn为最后的回调。reload为布尔,为false时,比如最后回调依赖于其他四种回调,一旦这四个回调都成功执行后,就执行最后回调,以后此四种回调每次执行,都会立即执行最后回调;若为true,则要重新再执行这四次回调才又执行最终回调。不写默认为false。
- flow.fire(name,ret),用于触发最后的回调,name为names中的某一单词,ret可选,它将成为最终回调的参数之一。
- flow.unbind(names,fn),移除监听,fn可选,不存在则移除names对应的所有回调。
好了,我们再回顾上面的QTE实现,其实现也不严谨,因为玩家可能一下子ASDFG都按了。另外,我们还要保证按键顺序,第一次必须是S,第二次是D,第三次是F。因此这种情况,使用操作流实现最合适了。上面游戏的QTE实现用mass Framework实现如下:
dom.require("ready,node,event,flow",function(){ var keystateTime = 0, i = 0; function checkInterval(){ if(!keystateTime){ keystateTime = new Date; }else{ var now = new Date - keystateTime; if(now > 500){ keystateTime = 0; } } } var flow = dom.flow("0_115,1_100,2_102", function(){ if( keystateTime){ i = -1; dom.log("开始攻击") } },true); dom(document).bind("keypress",function(e){ checkInterval(); dom.log(i+"_"+e.which) flow.fire(i+"_"+e.which); i++ if(i == 3 || e.which === 13 ){//按enter键重试 i = 0; } }); });
操作流有许多好处,我能说的大概被另一个类似实现EventProxy的作者说了,什么避免回调套嵌,将串行等待变成并行等待,一处合并,多处触发……EventProxy与我的操作流解决相同的问题,不同的是实现手段,我的操作流只是我的模块加载系统的简化版,使用依赖列表对应的对象的state的值来判定是否执行最后的回调,而EventProxy则由times决定是否执行最后的回调。
var event = dom.flow("1,2,3,4", function(){//这样就完全等于EventProxy 的 assignAll API for (var i=0; i!=4; ++i){ console.log(arguments[i]); } }); console.log("first"); event.fire("1", 1); event.fire("2", 2); event.fire("3", 3); event.fire("3", 3.5); event.fire("4", 4); console.log("second"); event.fire("1", 1); event.fire("2", 2); event.fire("3", 3); event.fire("4", 4); /** first 1 2 3.5 4 second 1 2 3.5 4 1 2 3.5 4 1 2 3 4 1 2 3 4 */
操作流大概能应对90%的异步操作,但面对存款取款这种事务式操作,还是很无力,我考虑是否要引进“锁”的概念。不过这是很久以后的事吧,因为一般的ORM应该能帮我们搞定这东西。介绍完毕。源码放在github上,自己去看。