JavaScript自定义事件
很多DOM对象都有原生的事件支持,向div就有click、mouseover等事件,事件机制可以为类的设计带来很大的灵活性,相信.net程序员深有体会。随着web技术发展,使用JavaScript自定义对象愈发频繁,让自己创建的对象也有事件机制,通过事件对外通信,能够极大提高开发效率。
简单的事件需求
事件并不是可有可无,在某些需求下是必需的。以一个很简单的需求为例,在web开发中Dialog很常见,每个Dialog都有一个关闭按钮,按钮对应Dialog的关闭方法,代码看起来大概是这样
<!DOCTYPE html> <html> <head> <title>Test</title> <style type="text/css" > .dialog { position:fixed; width:300px; height:300px;z-index:30;
top:50%; left:50%;
margin-top:-200px; margin-left:-200px;
box-shadow:2px 2px 4px #ccc;
background-color:#f1f1f1;
display:none;
}
.dialog .title
{
font-size:16px;
font-weight:bold;
color:#fff;
padding:4px;
background-color:#404040;
}
.dialog .close
{
width:20px;
height:20px;
margin:3px;
float:right;
cursor:pointer;
}
</style>
</head>
<body>
<inputtype="button" value="Dialog Test" onclick="openDialog();"/>
<divid="dlgTest" class="dialog">
<imgclass="close" alt="" src="images/close.png">
<divclass="title">Dialog</div>
<divclass="content">
</div>
</div>
<scripttype="text/javascript">
function Dialog(id){
this.id=id;
var that=this;
document.getElementById(id).children[0].onclick=function(){
that.close();
}
}
Dialog.prototype.show=function(){
var dlg=document.getElementById(this.id);
dlg.style.display='block';
dlg=null;
}
Dialog.prototype.close=function(){
var dlg=document.getElementById(this.id);
dlg.style.display='none';
dlg=null;
}
</script>
<scripttype="text/javascript">
function openDialog(){
var dlg=new Dialog('dlgTest');
dlg.show();
}
</script>
</body>
<html>
这样在点击button的时候就可以弹出Dialog,点击关闭按钮的时候隐藏Dialog,看起来不错实现了需求,但总感觉缺点儿什么,一般Dialog显示的时候页面还会弹出一层灰蒙蒙半透明的罩子,阻止页面其它地方的点击,Dialog隐藏的时候罩子去掉,页面又能够操作。加些代码添个罩子。
在body顶部添加一个pagecover
<div id="pageCover" class="pageCover"></div>
为其添加style
.pageCover { width:100%; height:100%; position:absolute; z-index:10; background-color:#666; opacity:0.5; display:none; }
为了打开的时候显示page cover,需要修改openDialog方法
function openDialog(){ var dlg=new Dialog('dlgTest'); document.getElementById('pageCover').style.display='block'; dlg.show(); }
效果很不错的样子,灰蒙蒙半透明的罩子在Dialog弹出后遮盖住了页面上的按钮,Dialog在其之上,这时候问题来了,关闭Dialog的时候page cover仍在,没有代码其隐藏它,看看打开的时候怎么显示的page cover,关闭的时候怎么隐藏行了! 还真不行,打开的代码是页面button按钮的事件处理程序自己定义的,在里面添加显示page cover的方法合情合理,但是关闭Dialog的方法是Dialog控件(虽然很简陋,远远算不上是控件)自己的逻辑,和页面无关,那修改Dialog的close方法可以吗?也不行!有两个原因,首先Dialog在定义的时候并不知道page cover的存在,这两个控件之间没有什么耦合关系,如果把隐藏page cover逻辑写在Dialog的close方法内,那么dialog是依赖于page cover的,也就是说页面上如果没有page cover,dialog就会出错。而且Dialog在定义的时候,也不知道特定页面的page cover id,没有办法知道隐藏哪个div,是不是在构造Dialog时把page cover id传入就可以了呢? 这样两个控件不再有依赖关系,也能够通过id查找到page cover DIV了,但是如果用户有的页面需要弹出page cover,有的不需要怎么办?
这是就事件大显身手的时候了,修改一下dialog 对象和openDialog方法
function Dialog(id){ this.id=id; this.close_handler=null; var that=this; document.getElementById(id).children[0].onclick=function(){ that.close(); if(typeof that.close_handler=='function') { that.close_handler(); } } }
function openDialog(){ var dlg=new Dialog('dlgTest'); document.getElementById('pageCover').style.display='block'; dlg.close_handler=function(){ document.getElementById('pageCover').style.display='none'; } dlg.show(); }
在Dialog对象内部添加一个句柄,关闭按钮的click事件处理程序在调用close方法后判断该句柄是否为function,是的话就调用执行该句柄。在openDialog方法中,创建Dialog对象后对句柄赋值为一隐藏page cover方法,这样在关闭Dialog的时候就隐藏了page cover,同时没有造成两个控件之间的耦合。这一交互过程就是一个简单的 定义事件——绑定事件处理程序——触发事件的过程,DOM对象的事件,比如button的click事件也是类似原理。
高级一点的自定义事件
上面举的小例子很简单,远远不及DOM本身事件精细,这种简单的事件处理有很多弊端
1.没有共同性。如果在定义一个控件,还得写一套类似的结构处理
2.事件绑定有排斥性。只能绑定了一个close事件处理程序,绑定新的会覆盖之前绑定
3.封装不够完善。如果用户不知道有个 close_handler的句柄,就没有办法绑定该事件,只能去查源代码
逐个分析一下这几个弊端,弊端一很熟悉,使用过面向对象的同学都可以轻易想到解决方法——继承;对于弊端二则可以提供一个容器(二维数组)来统一管理所有事件;弊端三的解决需要和弊端一结合在自定义的事件管理对象中添加统一接口用于添加/删除/触发事件
function EventTarget(){ this.handlers={}; } EventTarget.prototype={ constructor:EventTarget, addHandler:function(type,handler){ if(typeof this.handlers[type]=='undefined'){ this.handlers[type]=new Array(); } this.handlers[type].push(handler); }, removeHandler:function(type,handler){ if(this.handlers[type] instanceof Array){ var handlers=this.handlers[type]; for(var i=0,len=handlers.length;i<len;i++){ if(handler[i]==handler){ handlers.splice(i,1); break; } } } }, trigger:function(event){ if(!event.target){ event.target=this; } if(this.handlers[event.type] instanceof Array){ var handlers=this.handlers[event.type]; for(var i=0,len=handlers.length;i<len;i++){ handlers[i](event); } } } }
addHandler方法用于添加事件处理程序,removeHandler方法用于移除事件处理程序,所有的事件处理程序在属性handlers中统一存储管理。调用trigger方法触发一个事件,该方法接收一个至少包含type属性的对象作为参数,触发的时候会查找handlers属性中对应type的事件处理程序。写段代码测试一下。
function onClose(event){ alert('message:'+event.message); } var target=new EventTarget(); target.addHandler('close',onClose); //浏览器不能帮我们创建事件对象了,自己创建一个 var event={ type:'close', message:'Page Cover closed!' }; target.trigger(event);
至此后连个弊端一解决,应用一下继承解决第一个弊端,下面是寄生式组合继承的核心代码,这种继承方式是目前公认的JavaScript最佳继承方式
function extend(subType,superType){ var prototype=Object(superType.prototype); prototype.constructor=subType; subType.prototype=prototype; }
最后写成的版本就是这样的
<!DOCTYPE html> <html> <head> <title>Test</title> <style type="text/css" > html,body { height:100%; width:100%; padding:0; margin:0; } .dialog { position:fixed; width:300px; height:300px; top:50%; left:50%; margin-top:-200px; margin-left:-200px; box-shadow:2px 2px 4px #ccc; background-color:#f1f1f1; z-index:30; display:none; } .dialog .title { font-size:16px; font-weight:bold; color:#fff; padding:4px; background-color:#404040; } .dialog .close { width:20px; height:20px; margin:3px; float:right; cursor:pointer; } .pageCover { width:100%; height:100%; position:absolute; z-index:10; background-color:#666; opacity:0.5; display:none; } </style> </head> <body> <div id="pageCover" class="pageCover"></div> <input type="button" value="Dialog Test" onclick="openDialog();"/> <div id="dlgTest" class="dialog"> <img class="close" alt="" src="images/close.png"> <div class="title">Dialog</div> <div class="content"> </div> </div> <script type="text/javascript"> function EventTarget(){ this.handlers={}; } EventTarget.prototype={ constructor:EventTarget, addHandler:function(type,handler){ if(typeof this.handlers[type]=='undefined'){ this.handlers[type]=new Array(); } this.handlers[type].push(handler); }, removeHandler:function(type,handler){ if(this.handlers[type] instanceof Array){ var handlers=this.handlers[type]; for(var i=0,len=handlers.length;i<len;i++){ if(handler[i]==handler){ handlers.splice(i,1); break; } } } }, trigger:function(event){ if(!event.target){ event.target=this; } if(this.handlers[event.type] instanceof Array){ var handlers=this.handlers[event.type]; for(var i=0,len=handlers.length;i<len;i++){ handlers[i](event); } } } } </script> <script type="text/javascript"> function extend(subType,superType){ var prototype=Object(superType.prototype); prototype.constructor=subType; subType.prototype=prototype; } </script> <script type="text/javascript"> function Dialog(id){ EventTarget.call(this) this.id=id; var that=this; document.getElementById(id).children[0].onclick=function(){ that.close(); } } extend(Dialog,EventTarget); Dialog.prototype.show=function(){ var dlg=document.getElementById(this.id); dlg.style.display='block'; dlg=null; } Dialog.prototype.close=function(){ var dlg=document.getElementById(this.id); dlg.style.display='none'; dlg=null; this.trigger({type:'close'}); } </script> <script type="text/javascript"> function openDialog(){ var dlg=new Dialog('dlgTest'); dlg.addHandler('close',function(){ document.getElementById('pageCover').style.display='none'; }); document.getElementById('pageCover').style.display='block'; dlg.show(); } </script> </body> <html>
最后
这样解决了几个弊端看起来就完美多了,其实可以把打开Dialog显示page cover也写成类似关闭时事件的方式了。当代码中存在多个部分在特定时刻相互交互的情况下,自定义事件就非常有用了。如果每个对象都有其它对象的引用,那么整个代码高度耦合,对象改动会影响其它对象,维护起来困难重重。自定义事件使对象解耦,功能隔绝,这样对象之间实现了高聚合。