JavaScript自定義事件

隨手發表於2019-02-16

很多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();
        }

 

image

效果很不錯的樣子,灰濛濛半透明的罩子在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也寫成類似關閉時事件的方式了。當程式碼中存在多個部分在特定時刻相互互動的情況下,自定義事件就非常有用了。如果每個物件都有其它物件的引用,那麼整個程式碼高度耦合,物件改動會影響其它物件,維護起來困難重重。自定義事件使物件解耦,功能隔絕,這樣物件之間實現了高聚合。

相關文章