很多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 >
< 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 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 >
< script type ="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也寫成類似關閉時事件的方式了。當代碼中存在多個部分在特定時刻相互交互的情況下,自定義事件就非常有用了。如果每個對象都有其它對象的引用,那么整個代碼高度耦合,對象改動會影響其它對象,維護起來困難重重。自定義事件使對象解耦,功能隔絕,這樣對象之間實現了高聚合。