深入理解事件機制的實現


一、一個實例

假設你在你家客廳里玩游戲,口渴了,需要到廚房開一壺水,等水開了的時候,為了防止水熬干,你需要及時把火爐關掉。為了及時了解到水是否燒開,你有三種策略可以選擇:

1. 守在廚房內,等水燒開

這種策略顯然是很愚蠢的,采取這種策略,在燒水的過程中你將不能做任何事情,效率極低。

2. 呆在客廳玩游戲,每隔一兩分鍾跑到廚房看一次

這種策略,在計算機科學中稱為輪詢,即每隔一定的時間,監測一次。在這里,也是很不明智的,在玩游戲時需要不斷的分心。

3. 在水壺上安裝一個報警器,當水開了的時候,發出警報

這種策略是最好的,既不耽誤自己玩游戲,又能在水開了的時候使自己及時獲得通知。這種策略在計算機中通過事件機制來實現。

二、事件機制的組成

通過上面的實例,我們可以抽象出一個事件機制有三個組成部分:

1.事件源:即事件的發送者,在上例中為水壺;

2.事件:事件源發出的一種信息或狀態,比如上例的警報聲,它代表着水開了;

3.事件偵聽者:對事件作出反應的對象,比如上例中的你。在設計事件機制時一般把偵聽者設計為一個函數,當事件發送時,調用此函數。比如上例中可以把倒水設計為偵聽者。

三、初步實現

可以使用面向對象設計中的組合模式,把事件偵聽者當做事件源內部的一個對象,當事件發生時,調用偵聽者即可:

1 事件源:水壺{
2     事件偵聽者:你關火;//事件源持有事件偵聽者
3 
4     發送(事件:“水開了”){
5         你關火();
6     }
7 }

四、出現多個事件偵聽者的情況

如果你和你女朋友都在客廳玩游戲,水開的時候應該誰去關火呢?假設精明(懶惰)的你,聽到水開的警報聲,馬上假裝上廁所,你女朋友只能無奈地去關火。這種情況下,水壺發出的警報聲導致了兩個反應:1.你上廁所,2.你女朋友去關火。此時我們要如何實現呢?我們依然可以采用上面的實現方案,再在事件源中添加一個事件偵聽者:

 1 事件源:水壺{
 2     事件偵聽者:你上廁所;
 3     事件偵聽者:你女朋友關火;
 4 
 5     發送(事件:“水開了”){
 6             你上廁所();
 7             你女朋友關火();
 8     }
 9 
10 }

但這種設計有一個重大缺陷:事件源和事件偵聽者過度耦合。所有偵聽者都是硬編碼入事件源中,在程序執行過程中無法更改,靈活性極差。比如,有一天你女朋友外出了,只能你去關火,那么上面的事件源就需要重新修改。我們可以采用下面的方法使事件源和事件偵聽者解耦:

1.事件源中定義一個列表,比如數組,用來存儲所有偵聽者;

2.為列表留一個增刪數據的接口,用來隨時添加和刪除偵聽者;

3.當發送事件時,遍歷並執行列表中的偵聽者

實現如下:

 1 事件源:水壺{
 2 
 3     事件偵聽者:偵聽者列表[];
 4 
 5     添加事件偵聽者(偵聽者){
 6         偵聽者列表加入偵聽者
 7     }
 8     刪除事件偵聽者(偵聽者){
 9         偵聽者列表移除偵聽者
10     }
11 
12     發送(事件){
13       //遍歷並執行列表中的偵聽者
14       for(偵聽者 in 偵聽者列表){
15           執行偵聽者
16       }
17   }
18 
19 }

這種實現方案即為觀察者設計模式,可以讓偵聽者預訂事件。

五、事件源可發送多種事件的情況

假設你家的水壺有點智能,當水溫達到90度的時候,會發出一個“水快開了”的警報,為你提前逃到廁所偷懶留出了充足的時間,這種情況下的事件和偵聽者的對應關系如下:

我們可以在添加和刪除偵聽者的時候,把事件類型和偵聽者綁定成一個數組(或對象),再加入偵聽者列表。在發送事件時,在列表中查找和當前事件綁定的偵聽器執行:

事件源:水壺{

    事件偵聽者:偵聽者列表[];

    添加事件偵聽者(事件類型,偵聽者){
        帶類型偵聽者=[事件類型,偵聽者];//通過數組把事件類型和偵聽者綁定
        偵聽者列表加入帶類型偵聽者;
    }
    刪除事件偵聽者(事件類型,偵聽者){
        通過事件類型和偵聽者查找列表中對應的偵聽器刪除;
    }

    發送(事件類型){
        //遍歷並執行列表中的偵聽者
        for(帶類型偵聽者 in 偵聽者列表){
            if(帶類型偵聽者[0]==事件類型){
                    帶類型偵聽者[1]()//執行對應的偵聽器    
            }
        }
    }
}

把上面的文字描述翻譯成偽碼如下:

 1 //水壺類
 2 Kettle{
 3 
 4     array:Listeners[];
 5 
 6     addEventListener(eventType,listener){
 7         typeListener=[eventType,listener];//通過數組把事件類型和偵聽者綁定
 8         Listeners.push(typeListener);
 9     }
10 
11     removeEventListener(eventType,listener){
12         Listeners.delete([eventType,listener]);
13     }
14 
15     dispatch(eventType){
16         //遍歷並執行列表中的偵聽者
17         for(typeListener in Listeners){
18             if(typeListener[0]==eventType){
19                 typeListener[1]()//執行對應的偵聽器    
20             }
21         }
22     }
23 
24 }
25 
26 goWc(){
27     //你上廁所
28 }
29 
30 turnOffFire(){
31     //女朋友關火
32 }
33 
34 kettle=new Kettle();
35 //水壺注冊水快開了事件
36 kettle.addEventListener("水快開了",goWC);
37 kettle.addEventListener("水開了",turnOffFire);    
38 kettle.dispatch("水快開了");

優化:遵循"針對接口編程"的設計原則,應該為水壺、事件、偵聽器設計一個基類,其他具體的類繼承這些基類;

六、顯示對象上的事件:理解事件流

當事件發生在顯示對象上(比如瀏覽器)的時候,會遇到一個很有趣的問題:頁面的那一部分會擁有某個特定的事件?比如當你點擊頁面上的一棟小房子的時候,根據視角的遠近,你點擊的對象會發生變化。從最遠處來看你點擊的是頁面,鏡頭拉近你點擊的是小房子,再拉近你點擊的是房子上的一面牆,再拉近你點擊的是牆上的一塊磚。也就是說,你點擊一次頁面也許會有很多顯示對象發生了點擊事件,如果你在每一個顯示對象上都綁定了點擊處理程序,那么這些程序都會執行。這里會遇到一個問題:這些程序按什么順序執行。這取決於顯示對象接受到點擊事件的順序,一般有兩種模式:事件冒泡和事件捕獲。這種事件在顯示對象上按順序發生的過程稱為事件流。

1. 事件冒泡

事件冒泡,即事件開始時由最具體的元素(比如上例的磚塊)接受,然后逐級向上傳播到較為不具體的節點(文檔);

2. 事件捕獲

事件捕獲的思想是不太具體的元素(文檔)更早的接受事件,而最具體的元素最后接受到事件(磚塊)。事件捕獲的用意在於事件到達預訂目標之間捕獲它。

 

在JavaScript中為DOM中的元素添加事件處理程序時,有三個參數,其中第三個參數是一個布爾值,當為true時,表示在捕獲階段調用事件處理程序,為false時,表示在冒泡階段調用事件處理程序,舉例如下:

 1 <body>
 2     <div id="outer">
 3         <div id="inner" >
 4         </div>
 5     </div>
 6 </body>
 7 
 8 //例一
 9 var btn1=document.getElementById("outer");
10 btn1.addEventListener("click",function(){
11     alert('outer')
12 },false);
13 
14 var btn2=document.getElementById("inner");
15 btn2.addEventListener("click",function(){
16     alert('inner')
17 },false);
18 
19 //例二
20 var btn1=document.getElementById("outer");
21 btn1.addEventListener("click",function(){
22     alert('outer')
23 },false);
24 
25 var btn2=document.getElementById("inner");
26 btn2.addEventListener("click",function(){
27     alert('inner')
28 },false);    

上面例一的事件處理程序都發生在冒泡階段,所以會先輸出inner,再輸出outer。例二中id為outer元素上的事件處理程序發生在捕獲階段,所以會先輸出outer,再輸出inner。

注意:事件流發生在父元素和子元素之間,而不是兩個同級的元素。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM