Ruby's Louvre

每天学习一点点算法

导航

javascript 操作流——回调的回调

操作流是应对一个函数的执行依赖于多个异步操作的结果而产生的。这其实是事件派发的一种。用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上,自己去看。

posted on 2011-11-22 00:36  司徒正美  阅读(3783)  评论(5编辑  收藏  举报