JavaScript事件模型及事件代理


事件模型

  JavaScript事件使得網頁具備互動和交互性,我們應該對其深入了解以便開發工作,在各式各樣的瀏覽器中,JavaScript事件模型主要分為3種:原始事件模型、DOM2事件模型、IE事件模型。

  1.原始事件模型(DOM0級)

    這是一種被所有瀏覽器都支持的事件模型,對於原始事件而言,沒有事件流,事件一旦發生將馬上進行處理,有兩種方式可以實現原始事件:

      (1)在html代碼中直接指定屬性值:<button id="demo" type="button" onclick="doSomeTing()" />  

      (2)在js代碼中為 document.getElementsById("demo").onclick = doSomeTing()

    優點:所有瀏覽器都兼容

    缺點:1)邏輯與顯示沒有分離;2)相同事件的監聽函數只能綁定一個,后綁定的會覆蓋掉前面的,如:a.onclick = func1; a.onclick = func2;將只會執行func2中的內容。3)無法通過事件的冒泡、委托等機制(后面會講到)完成更多事情。

    因為這些缺點,雖然原始事件類型兼容所有瀏覽器,但仍不推薦使用。

  2.DOM2事件模型

    此模型是W3C制定的標准模型,現代瀏覽器(IE6~8除外)都已經遵循這個規范。W3C制定的事件模型中,一次事件的發生包含三個過程:

    (1).事件捕獲階段,(2).事件目標階段,(3).事件冒泡階段。如下圖所示

 

  事件捕獲:當某個元素觸發某個事件(如onclick),頂層對象document就會發出一個事件流,隨着DOM樹的節點向目標元素節點流去,直到到達事件真正發生的目標元素。在這個過程中,事件相應的監聽函數是不會被觸發的。

  事件目標:當到達目標元素之后,執行目標元素該事件相應的處理函數。如果沒有綁定監聽函數,那就不執行。

  事件冒泡:從目標元素開始,往頂層元素傳播。途中如果有節點綁定了相應的事件處理函數,這些函數都會被一次觸發。

  所有的事件類型都會經歷事件捕獲但是只有部分事件會經歷事件冒泡階段,例如submit事件就不會被冒泡。 

  事件的傳播是可以阻止的:
  • 在W3c中,使用stopPropagation()方法
  • 在IE下設置cancelBubble = true;
  在捕獲的過程中stopPropagation();后,后面的冒泡過程就不會發生了。

  標准的事件監聽器該如何綁定:

    addEventListener("eventType","handler","true|false");其中eventType指事件類型,注意不要加‘on’前綴,與IE下不同。第二個參數是處理函數,第三個即用來指定是否在捕獲階段進行處理,一般設為false來與IE保持一致(默認設置),除非你有特殊的邏輯需求。監聽器的解除也類似:removeEventListner("eventType","handler","true!false");

  3.IE事件模型

  IE不把該對象傳入事件處理函數,由於在任意時刻只會存在一個事件,所以IE把它作為全局對象window的一個屬性,為求證其真偽,使用IE8執行代碼alert(window.event),結果彈出是null,說明該屬性已經定義,只是值為null(與undefined不同)。難道這個全局對象的屬性是在監聽函數里才加的?於是執行下面代碼:

    window.onload = function (){alert(window.event);}

    setTimeout(function(){alert(window.event);},2000);

    結果第一次彈出【object event】,兩秒后彈出依然是null。由此可見IE是將event對象在處理函數中設為window的屬性,一旦函數執行結束,便被置為null了。IE的事件模型只有兩步,先執行元素的監聽函數,然后事件沿着父節點一直冒泡到document。冒泡已經講解過了,這里不重復。IE模型下的事件監聽方式也挺獨特,綁定監聽函數的方法是:attachEvent( "eventType","handler"),其中evetType為事件的類型,如onclick,注意要加’on’。解除事件監聽器的方法是 detachEvent("eventType","handler" )

    IE的事件模型已經可以解決原始模型的三個缺點,但其自己的缺點就是兼容性,只有IE系列瀏覽器才可以這樣寫。

   以上就是3種事件模型,在我們寫代碼的時候,為了兼容ie,通常使用以下寫法:

    var demo = document.getElementById('demo');

    if(demo.attachEvent){

     demo.attachEvent('onclick',func);

    }else{

     demo.addEventListener('click',func,false);

    }

event詳解

  上面已經講解了3種事件模型,事件,大部分情況下指的是用戶的鼠標動作和鍵盤動作,如點擊、移動鼠標、按下某個鍵,為什么說大部分呢,因為事件不單單只有這兩部分,還有其他的例如document的load和unloaded。那么事件在瀏覽器中,到底包含哪些信息呢?

  事件被封裝成一個event對象,包含了該事件發生時的所有相關信息(event的屬性)以及可以對事件進行的操作(event的方法)。

  我為下圖中的button綁定了一個點擊事件,然后將event輸出到控制台:

 

  可以看到是一個MouseEvent對象,包含了一系列屬性,如鼠標點擊的位置等。那么敲擊鍵盤時產生的event對象和它一樣嗎?看看就知道: 

  可以看到是一個KeyboardEvent對象,屬性跟上面的也不太一樣,如沒有clientX/Y(敲鍵盤怎么能獲取到鼠標的位置呢)。不管是MouseEvent還是KeyboardEvent或是其他類型,都是繼承自一個叫Event的類。

  event對象常用屬性、方法:

  1. 事件定位相關屬性

  如果你細細看了MouseEvent對象里的屬性,一定發現了有很多帶X/Y的屬性,它們都和事件的位置相關。具體包括:x/y、clientX/clientY、pageX/pageY、screenX/screenY、layerX/layerY、offsetX/offsetY 六對。為什么有這么多X-Y啊?不要着急,作為一個web開發者,你應該了解各瀏覽器之間是有差異的,這些屬性都有各自的意思:

    x/y與clientX/clientY值一樣,表示距瀏覽器可視區域(工具欄除外區域)左/上的距離;

    pageX/pageY,距頁面左/上的距離,它與clientX/clientY的區別是不隨滾動條的位置變化;

    screenX/screenY,距計算機顯示器左/上的距離,拖動你的瀏覽器窗口位置可以看到變化;

    layerX/layerY與offsetX/offsetY值一樣,表示距有定位屬性的父元素左/上的距離。

 

 下面列出了各屬性的瀏覽器支持情況。(+支持,-不支持)    

offsetX/offsetY W3C-  IE+  Firefox- Opera+  Safari+ chrome+
x/y W3C-  IE+  Firefox- Opera+  Safari+ chrome+
layerX/layerY W3C-  IE-  Firefox+ Opera-  Safari+ chrome+
pageX/pageY W3C-  IE+- Firefox+ Opera+  Safari+ chrome+
clientX/clientY W3C+  IE+  Firefox+ Opera+  Safari+ chrome+
screenX/screenY W3C+  IE+  Firefox+ Opera+  Safari+ chrome+

        注意:該表摘自其他文章,我未做全部驗證,但是最新版本的現代瀏覽器,這些屬性貌似是都支持了,為了更好的兼容性,通常選擇W3C支持的屬性。

  2.其他常用屬性

    target:發生事件的節點;

    currentTarget:當前正在處理的事件的節點,在事件捕獲或冒泡階段;

      timeStamp:事件發生的時間,時間戳。

    bubbles:事件是否冒泡。

    cancelable:事件是否可以用preventDefault()方法來取消默認的動作;

    keyCode:按下的鍵的值;

  3. event對象的方法

    event. preventDefault()//阻止元素默認的行為,如鏈接的跳轉、表單的提交;

    event. stopPropagation()//阻止事件冒泡

    event.initEvent()//初始化新事件對象的屬性,自定義事件會用,不常用

    event. stopImmediatePropagation()//可以阻止掉同一事件的其他優先級較低的偵聽器的處理(這貨表示沒用過,優先級就不說明了,谷歌或者問度娘吧。)

  event.target與event.currentTarget他們有什么不同?

  target在事件流的目標階段;currentTarget在事件流的捕獲,目標及冒泡階段。只有當事件流處在目標階段的時候,兩個的指向才是一樣的, 而當處於捕獲和冒泡階段的時候,target指向被單擊的對象而currentTarget指向當前事件活動的對象(一般為父級)。

  

事件觸發器

  前面提到的事件都是依靠用戶或者瀏覽器自帶事件去觸發的,比如click是用戶點擊事件目標觸發,load是指定元素已載入的時候瀏覽器的行為事件,等等,如果只有在這些條件下才能觸發事件,那么我們的自定義事件如何觸發呢?

  事件觸發器就是用來觸發某個元素下的某個事件,當然也可以用來觸發自定義事件IE下fireEvent方法,現代瀏覽器(chrome,firefox等)有dispatchEvent方法。

  

 

  這里先介紹下自定義事件(事件模擬):

自定義事件

  想要實現一個自定義事件,需要經過下面幾步:

  1.createEvent(eventType)

    事件被封裝成一個event對象,這在上面已經說過了,我們想要自定義一個事件,js中有這么一個方法createEvent(eventType),見名知義,顯然是用於“創造”一個事件的,沒錯,要想自定義事件,首先,我們得“創造”一個事件。

     參數:eventType 共5種類型:
       Events :包括所有的事件. 
           HTMLEvents:包括 'abort', 'blur', 'change', 'error', 'focus', 'load', 'reset', 'resize', 'scroll', 'select', 
                                    'submit', 'unload'. 事件
           UIEevents :包括 'DOMActivate', 'DOMFocusIn', 'DOMFocusOut', 'keydown', 'keypress', 'keyup'.
                                  間接包含 MouseEvents. 
           MouseEvents:包括 'click', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup'. 
           MutationEvents:包括 'DOMAttrModified', 'DOMNodeInserted', 'DOMNodeRemoved', 
                                      'DOMCharacterDataModified', 'DOMNodeInsertedIntoDocument', 
                                      'DOMNodeRemovedFromDocument', 'DOMSubtreeModified'.     

  2. 在createEvent后必須初始化,為大家介紹5種對應的初始化方法
      HTMLEvents 和 通用 Events:
                 initEvent( 'type', bubbles, cancelable )
        UIEvents:
                      initUIEvent( 'type', bubbles, cancelable, windowObject, detail )
       MouseEvents: 
                      initMouseEvent( 'type', bubbles, cancelable, windowObject, detail, screenX, screenY, 
                      clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget )
       MutationEvents :
                      initMutationEvent( 'type', bubbles, cancelable, relatedNode, prevValue, newValue, 
                      attrName, attrChange ) 

    這里重點介紹MouseEvents(鼠標事件模擬):

    鼠標事件可以通過創建一個鼠標事件對象來模擬(mouse event object),並且授予他一些相關信息,創建一個鼠標事件通過傳給createEvent()方法一個字符串“MouseEvents”,來創建鼠標事件對象,之后通過iniMouseEvent()方法來初始化返回的事件對象,iniMouseEvent()方法接受15參數,參數如下:
     type string類型 :要觸發的事件類型,例如‘click’。
     bubbles Boolean類型:表示事件是否應該冒泡,針對鼠標事件模擬,該值應該被設置為true。
     cancelable bool類型:表示該事件是否能夠被取消,針對鼠標事件模擬,該值應該被設置為true。
          view 抽象視圖:事件授予的視圖,這個值幾乎全是document.defaultView.
      detail int類型:附加的事件信息這個初始化時一般應該默認為0。
      screenX int類型 : 事件距離屏幕左邊的X坐標
      screenY int類型 : 事件距離屏幕上邊的y坐標
      clientX int類型 : 事件距離可視區域左邊的X坐標
      clientY int類型 : 事件距離可視區域上邊的y坐標
      ctrlKey Boolean類型 : 代表ctrol鍵是否被按下,默認為false。
      altKey Boolean類型 : 代表alt鍵是否被按下,默認為false。
      shiftKey Boolean類型 : 代表shif鍵是否被按下,默認為false。
      metaKey Boolean類型: 代表meta key 是否被按下,默認是false。
      button int類型: 表示被按下的鼠標鍵,默認是零. 
      relatedTarget (object) : 事件的關聯對象.只有在模擬mouseover 和 mouseout時用到。

    如果你想要了解更多事件模擬參數詳解,請查看這篇文章,http://www.cnblogs.com/MrBackKom/archive/2012/06/26/2564501.html。或者查看《javascript高級程序設計》的模擬事件章節

  3. 在初始化完成后就可以隨時觸發需要的事件了,為大家介紹targetObj.dispatchEvent(event)使targetObj對象的event事件觸發。(IE上請用fireEvent方法)
    4. 例子
      //例子1  立即觸發鼠標被按下事件

?
1
2
3
4
var fireOnThis = document.getElementById( 'demo' );
var evObj = document.createEvent( 'MouseEvents' );
evObj.initMouseEvent( 'click' , true , true , window, 1, 12, 345, 7, 220, false , false , true , false , 0, null );
fireOnThis.dispatchEvent(evObj);

    //例子2  考慮兼容性的一個鼠標移動事件

?
1
2
3
4
5
6
7
8
9
var fireOnThis = document.getElementById( 'someID' );
if ( document.createEvent ) {
      var evObj = document.createEvent( 'MouseEvents' );
      evObj.initEvent( 'mousemove' , true , false );
      fireOnThis.dispatchEvent(evObj);
  } else if ( document.createEventObject )
  {
      fireOnThis.fireEvent( 'onmousemove' );
  }

  

  事件代理

  傳統的事件處理中,我們為每一個需要觸發事件的元素添加事件處理器,但是這種方法將可能會導致內存泄露或者性能下降(特別是通過ajax獲取數據后重復綁定事件,總之,越頻繁風險越大)。事件代理在js中是一個非常有用但對初學者稍難理解的功能,我們將事件處理器綁定到一個父級元素上,避免了頻繁的綁定多個子級元素,依靠事件冒泡機制與事件捕獲機制,子級元素的事件將委托給父級元素。事件冒泡與捕獲在上面事件模型中已經講解過。

  有了事件捕獲和冒泡的認識后,下面舉例說明事件代理:

  假設我們有一個列表,列表中的每一個li和li中的span都需要綁定某個事件處理函數。如下代碼:

 

?
1
2
3
4
5
6
7
8
<ul id= "parent-ul" >
    <li><span>Item 1</span></li>
    <li><span>Item 2</span></li>
     <li><span>Item 3</span></li>
     <li><span>Item 4</span></li>
     <li><span>Item 5</span></li>
     <li><span>Item 6</span></li>
</ul>

  

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
~( function () {
     var ParentNode = document.querySelector( "#parent-ul" );
     var targetNodes = ParentNode.querySelectorAll( "li" );
     var spanNodes = ParentNode.querySelectorAll( "span" );
      
     //綁定事件處理函數
     for ( var i=0, l = targetNodes.length; i < l; i++){
         addListenersToLi(targetNodes[i]);
     }
     for ( var i=0, l = spanNodes.length; i < l; i++){
         addListenersToSpan(spanNodes[i]);
     }
 
     //事件處理函數
     function addListenersToLi(targetNode){
         targetNode.onclick = function targetClick(e){
             if (e.target && e.target.nodeName.toUpperCase() == "LI" ) {
                 console.log( "當你看見我的時候,LI點擊事件已經生效!" );
             }
         };
     }
     function addListenersToSpan(targetNode){
         targetNode.onclick = function targetClick(e){
             if (e.target && e.target.nodeName.toUpperCase() == "SPAN" ) {
                 console.log( "當你看見我的時候,SPAN點擊已經生效!" );
             }
         };
     }
})();

  

  

  這里為li和span元素都添加了onclick事件處理函數,但是如果這些li和span有可能被刪除或者新增,那么總是需要為新增的li、span元素重新綁定事件,這種寫法使得我們很苦惱,除了開頭提到的問題外,增加了代碼量而且代碼看上去不太整潔了,那么使用事件代理會怎么樣呢?如下代碼:

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
~( function () {
     // 獲取li的父節點,並為其添加一個click事件
     document.getElementById( "parent-ul" ).addEventListener( "click" , function (e) {
         // 檢查事件源e.targe是否為span
         if (e.target && e.target.nodeName.toUpperCase() == "SPAN" ) {
             // 真正的處理過程在這里
             console.log( "當你看見我的時候,SPAN事件代理已經生效!" );
         }<br>      //檢查事件源e.target是否為li
         if (e.target && e.target.nodeName.toUpperCase() == "LI" ) {
             // 真正的處理過程在這里
             console.log( "當你看見我的時候,LI事件代理已經生效!" );
         }
     });
})();

  我們改變了思路,為li、span的父級元素即id為parent-ul的ul元素添加了一click事件,當點擊事件發生時,我們可以通過e.target捕獲事件目標,並通過e.target.nodeName.toUpperCase== "LI"來判斷事件目標是否為li(span同理),如是那么執行相應的事件處理程序。使用這樣的方式有利於解決前面提到的一些問題:

  1.最直接的就是,代碼更整潔了,而且可讀性更強。
  2.對於動態化的頁面(如本例,li、span會新增和刪除),不用頻繁的綁定事件,減少了內存泄露的概率。

 注意:不是所有的事件都能冒泡的。blur、focus、load和unload不能像其它事件一樣冒泡。事實上blur和focus可以用事件捕獲而非事件冒泡的方法獲得(在IE之外的其它瀏覽器中)。 
    在管理鼠標事件的時候有些需要注意的地方。如果你的代碼處理mousemove事件的話你遇上性能瓶頸的風險可就大了,因為mousemove事件觸發非常頻繁。而mouseout則因為其怪異的表現而變得很難用事件代理來管理。 

 jq事件代理:jq為提供了delegate()函數處理事件代理,這里不多介紹,個人在工作中使用on()函數解決一些事件代理問題(使用更方便),解決上訴例子的代碼如下

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
~( function () {
       $( "#parent-ul" ).on( "click" , "li,span" , function (e) {           
         if ($( this )[0].nodeName== "SPAN" ) {
               // 真正的處理過程在這里
               console.log( "當你看見我的時候,SPAN事件代理已經生效!" );
               //這里要阻止冒泡,不然點擊span時會觸發li的事件
               e.stopPropagation();
            }
             
           if ($( this )[0].nodeName== "LI" ) {
                 // 真正的處理過程在這里
                 console.log( "當你看見我的時候,LI事件代理已經生效!" );
             }
       });
  })();   

  如果你使用jq,推薦使用on()方法。

 

  

 


免責聲明!

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



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