js自定义消息机制研究学习(四)之杂七杂八

来源:岁月联盟 编辑:exp 时间:2011-08-12

终于要写完了~~^_^,期间给同事做了一次培训,写一次,讲一次的好处是,再次加深了自己对于消息、事件以及观察者模式的理解。

 

对我来说,讲清楚比写代码要难上很多。

 

这里分享一些与消息机制相关的一些杂七杂八的内容。

 

 

 

一、可测试的代码

 

早些时候,我向锐同学描述我的js程序结构,他问了我一个问题:你的js代码可测么?

 

我蒙了~虽然一直关注敏捷,一直也向往测试驱动开发,但还从没想过js代码的可测试(当然,也有测试,但基本上整测加局部测试),没有想过js的测试驱动。

 

当时,我迟疑了一会,才说应该是可测的。

 

写完上一篇文章(原谅我,觉得太简单,直接写的,忘了测试驱动),回头看了看,还好,基于消息的代码确实可以做到可测。

 

比如:

 

Animal

function Animal(config){    config=config || {};    var othis=this;    this.name=config["name"] || "Anonymous";    this.x=config["x"] || 0;    var toward=1;//右:1,左-1    var __timeout=0;    var __speed=5;    //说    this.say=function(str)    {        this.trigger({type:"say",msg:str});        return str;    }    //停    this.stop=function()    {        clearTimeout(__timeout);         this.trigger({type:"stop",x:this.x});        return this.x;    }    //跑动    this.run=function(speed)    {        __speed=speed || __speed;        this.x+=__speed*toward;        __timeout=setTimeout(function(){ othis.run();},100);        this.trigger({type:"run",x:this.x,toward:toward,speed:__speed});        return {x:this.x,toward:toward,speed:__speed};    }    //向左转    this.turnLeft=function()    {        toward=-1;        this.trigger({type:"turn",toward:toward});        return toward;    }    //向右转    this.turnRight=function()    {        toward=1;        this.trigger({type:"turn",toward:toward});        return toward;    }}方法都是有返回值,这样便于我们做一些单元测试,除了单元测试,我们还要做一些基于消息的机制的测试,构造一个伪对象去侦听Animal对象发送的消息

 

但这个Animal并不完全,还有一些不可测的,比如toward,它是完全封闭在内部的一个变量,你想要知道Animal的对象前进的方向,就有些困难。

 

不过这主要源于Animal这个类得不完全,但我们不能为了访问toward而直接把它修改为this.toward暴露出来,这样别人有可能赋一个错误的值:this.toward=100;仔细看一下代码,公开toward让人可以随别赋值是很危险的一件事情。好的方式,写一个getToward()方法。

 

一种难以测试的实现是示例这样写的:

 

Logger

///记录器function Logger(){     var dom=document.getElementById("log");      var log=function(str)      {          var time=new Date();          this.dom.innerHTML+="<br/>"+str+'<span style="color:gray">('+time.getHours()+":"+time.getMinutes()+":"+time.getSeconds()+")</span>";      };      this.handler=function(data,p){          switch(data.type)          {              case "say":                  this.log(p.name+"  说:"+data.msg);                  break;              case "stop":                  this.log('<span style="color:green">'+p.name+" 停在了"+data.x+'</span>');                  break;              case "turn":                  this.log(p.name+" 转向了"+(data.toward==1?"右":"左"));                  break;          }            }; }Logger对象只暴露了一个handler方法,它写死了dom。

 

当然它在示例运行中会很好的执行自己的职责。为了测试它,我们首先保证页面上要有一个id为log的dom元素,还要伪造一个消息对象,如Animal对象去给它发消息。这让我们又一种整体测试的感觉。

 

这不是一个好的示例。

 

 

总体而言,消息的密闭性会给测试带来一些麻烦。实际中,我们一个函数的调用可能会触发很多个消息,而不只是一个。而这些消息名又洒落在了代码的各处。除非你仔细的阅读代码,否则很可能会遗漏消息。

 

像Animal一样,尽量做到一个方法值触发一个消息,或者相反、相关联的消息。如果是一些大的对象,把消息名罗列在对象开始前得注释代码中,也便于他们维护调试。

 

 

 

二、冒泡的消息

 

在刚开始的时候,我曾实现过消息的冒泡,就像是内部a标签的click事件会冒泡到外部div一样。

 

消息的冒泡示例:

 

a.bind("test",b) b.bind("test",c) c.bind("test",d)

 

如果你实现了冒泡,a的test消息会沿着a->b->c->d的路径一直传送到d

 

简单的实现呢,就是每个对象的handler函数,直接trigger一下传进来的消息,这样的方式并没有实现自动化。一个简单改动如下:

 

View Code

function trigger(Y){            var queue =[];            var qs=this.__MSG_QS__ || {};             var sqs=this.__STATIC_MSG_QS__|| {};             queue= queue.concat(qs[Y.type] || []);            queue= queue.concat(sqs[Y.type] || []);            for (var a = 0, X = queue.length; a < X; a++) {               if(queue[a].handler)               {                   queue[a].handler(Y,this)                   if(queue[a].trigger)                   {                          queue[a].trigger(Y);                    }               }               else               {                   queue[a].call(this,Y,this);                }            }        } 重点看一下这一个改动:

 

if(queue[a].trigger)     {            queue[a].trigger(Y);     }如果发现观察者是一个monitor模式对象,那么调用它的trigger

 

这样,我们的消息就可以实现冒泡了。我们也可以为对象添加一个属性,标示对象是否允许冒泡,也可以再添加stopPropagation一个来阻止冒泡。

 

 

 

关于消息的冒泡,我的建议是谨慎使用。自定义对象不同于dom,dom是有层次结构的,dom只对父元素冒泡。

 

自定义对象是没有层次的(除非强制),有时我们甚至可以让对象自己监听自己的消息,很多时候,有很多对象侦听你的消息。实质的讲,此时的消息流,并不像冒泡,而是会有分支,在处理不好的情况下会有闭环,会有类似递归一样的自调用,自调用、闭环的情况都会引起死循环。

 

当然,你也可以使用一个字典来存储消息已经传递过的对象(注入到消息体内,或作为trigger另一个参数),防止闭环。但这样又会使你的monitor代码增加很多的处理。

 

 

 

 

 

 

三、异步的消息

 

之前,我所举的例子、代码,都是同步消息,消息调用,监听函数就会执行,并且只有所有的监听函数执行完毕,trigger的调用才结束。

 

 

 

 

比如:

 

obj.trigger({type:"test1"});

 

obj.trigger({type:"test2"});

 

test2的消息一定是在test1消息调用结束后才调用。

 

异步的消息,只是我的一个想法,利用setTimeout函数,用时间片得形式,一次只调用一个监听者,那么在消息源对象调用trigger方法,trigger很快可以执行完毕,而真正的消息处理呢,是放在了时间片中,慢慢的处理。

 

目前我没有用到需要异步消息处理的需求,对于设想的方案,没有做过测试。有兴趣的人可以自己研究一下。

 

估计有的js框架有类似的功能,可惜我没有研究过。

 

 

 

 

 

四、避免重复注册及注销观察者

 

 

 

 

如果你a.bind("test",b)两次,会发生什么情况?

 

a对象的test的事件,会很忠实地调用两次b。

 

为了避免重复,直接的方式就是一遍列表,判断一下b是否存在,这样的效率很低下。一种方式是为需要bind的对象,增加一个唯一标示,在monitor内增加一个函数(或者在全局增加一个函数),代码如下

 

var  monitor= (function(){//……       var __guid=0;       function guid(){               return ++__guid;        }         function bind(b){            var queue = this.__MSG_QS__=this.__MSG_QS__ || {};            if (!queue[b]) {                queue[b] = {}            }            for (var a = 1, X = arguments.length, Y; a < X; a++) {                var _guid=arguments[a].__guid=arguments[a].__guid || guid();                if( queue[b][_guid])                       queue[b][_guid]=arguments[a];            }        }//……})();相应的trigger里也要做一些相应的改变(以及live),这里不再给出代码,有兴趣的自己实现

 

注销是bind的反操作,如果你没有使用为对象赋唯一标示的方式,你需要用循环去判断对象是否在观察者队列中,如果在则从队列中移除。如果使用唯一标示,操作比较简单,使用delete即可。