JS中的事件、事件冒泡和事件捕獲、事件委托


https://www.cnblogs.com/diver-blogs/p/5649270.html

https://www.cnblogs.com/Chen-XiaoJun/p/6210987.html

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樹的節點向目標元素節點流去,直到到達事件真正發生的目標元素。在這個過程中,事件相應的監聽函數是不會被觸發的。

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

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

  IE 5.5: div -> body -> document 

  IE 6.0: div -> body -> html -> document 

  Mozilla 1.0: div -> body -> html -> document -> window 

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

  事件的傳播是可以阻止的:
  • 在W3c中,使用stopPropagation()方法
  • 在IE下設置eve.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()//可以阻止掉同一事件的其他優先級較低的偵聽器的處理(這貨表示沒用過,優先級就不說明了,谷歌或者問度娘吧。)

    4. 阻止事件的默認行為,例如click <a>后的跳轉~ 

  • 在W3c中,使用preventDefault()方法; 
  • 在IE下設置window.event.returnValue = false; 

  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()方法。

 
              

概述:

那什么叫事件委托呢?它還有一個名字叫事件代理,JavaScript高級程序設計上講:事件委托就是利用事件冒泡,只指定一個事件處理程序,就可以管理某一類型的所有事件。那這是什么意思呢?網上的各位大牛們講事件委托基本上都用了同一個例子,就是取快遞來解釋這個現象,我仔細揣摩了一下,這個例子還真是恰當,我就不去想別的例子來解釋了,借花獻佛,我摘過來,大家認真領會一下事件委托到底是一個什么原理:

有三個同事預計會在周一收到快遞。為簽收快遞,有兩種辦法:一是三個人在公司門口等快遞;二是委托給前台MM代為簽收。現實當中,我們大都采用委托的方案(公司也不會容忍那么多員工站在門口就為了等快遞)。前台MM收到快遞后,她會判斷收件人是誰,然后按照收件人的要求簽收,甚至代為付款。這種方案還有一個優勢,那就是即使公司里來了新員工(不管多少),前台MM也會在收到寄給新員工的快遞后核實並代為簽收。

這里其實還有2層意思的:

第一,現在委托前台的同事是可以代為簽收的,即程序中的現有的dom節點是有事件的;

第二,新員工也是可以被前台MM代為簽收的,即程序中新添加的dom節點也是有事件的。

為什么要用事件委托:

一般來說,dom需要有事件處理程序,我們都會直接給它設事件處理程序就好了,那如果是很多的dom需要添加事件處理呢?比如我們有100個li,每個li都有相同的click點擊事件,可能我們會用for循環的方法,來遍歷所有的li,然后給它們添加事件,那這么做會存在什么影響呢?

在JavaScript中,添加到頁面上的事件處理程序數量將直接關系到頁面的整體運行性能,因為需要不斷的與dom節點進行交互,訪問dom的次數越多,引起瀏覽器重繪與重排的次數也就越多,就會延長整個頁面的交互就緒時間,這就是為什么性能優化的主要思想之一就是減少DOM操作的原因;如果要用事件委托,就會將所有的操作放到js程序里面,與dom的操作就只需要交互一次,這樣就能大大的減少與dom的交互次數,提高性能;

每個函數都是一個對象,是對象就會占用內存,對象越多,內存占用率就越大,自然性能就越差了(內存不夠用,是硬傷,哈哈),比如上面的100個li,就要占用100個內存空間,如果是1000個,10000個呢,那只能說呵呵了,如果用事件委托,那么我們就可以只對它的父級(如果只有一個父級)這一個對象進行操作,這樣我們就需要一個內存空間就夠了,是不是省了很多,自然性能就會更好。

事件委托的原理:

事件委托是利用事件的冒泡原理來實現的,何為事件冒泡呢?就是事件從最深的節點開始,然后逐步向上傳播事件,舉個例子:頁面上有這么一個節點樹,div>ul>li>a;比如給最里面的a加一個click點擊事件,那么這個事件就會一層一層的往外執行,執行順序a>li>ul>div,有這樣一個機制,那么我們給最外面的div加點擊事件,那么里面的ul,li,a做點擊事件的時候,都會冒泡到最外層的div上,所以都會觸發,這就是事件委托,委托它們父級代為執行事件。

事件委托怎么實現:

終於到了本文的核心部分了,哈哈,在介紹事件委托的方法之前,我們先來看一段一般方法的例子:

子節點實現相同的功能:

?
1
2
3
4
5
6
< ul id = "ul1" >
   < li >111</ li >
   < li >222</ li >
   < li >333</ li >
   < li >444</ li >
</ ul >

實現功能是點擊li,彈出123:

?
1
2
3
4
5
6
7
8
9
window.onload = function (){
   var oUl = document.getElementById( "ul1" );
   var aLi = oUl.getElementsByTagName( 'li' );
   for ( var i=0;i<aLi.length;i++){
     aLi[i].onclick = function (){
       alert(123);
     }
   }
}

上面的代碼的意思很簡單,相信很多人都是這么實現的,我們看看有多少次的dom操作,首先要找到ul,然后遍歷li,然后點擊li的時候,又要找一次目標的li的位置,才能執行最后的操作,每次點擊都要找一次li;

那么我們用事件委托的方式做又會怎么樣呢?

?
1
2
3
4
5
6
window.onload = function (){
   var oUl = document.getElementById( "ul1" );
   oUl.onclick = function (){
     alert(123);
   }
}

這里用父級ul做事件處理,當li被點擊時,由於冒泡原理,事件就會冒泡到ul上,因為ul上有點擊事件,所以事件就會觸發,當然,這里當點擊ul的時候,也是會觸發的,那么問題就來了,如果我想讓事件代理的效果跟直接給節點的事件效果一樣怎么辦,比如說只有點擊li才會觸發,不怕,我們有絕招:

Event對象提供了一個屬性叫target,可以返回事件的目標節點,我們成為事件源,也就是說,target就可以表示為當前的事件操作的dom,但是不是真正操作dom,當然,這個是有兼容性的,標准瀏覽器用ev.target,IE瀏覽器用event.srcElement,此時只是獲取了當前節點的位置,並不知道是什么節點名稱,這里我們用nodeName來獲取具體是什么標簽名,這個返回的是一個大寫的,我們需要轉成小寫再做比較(習慣問題):

?
1
2
3
4
5
6
7
8
9
10
11
window.onload = function (){
              var oUl = document.getElementById( "ul1" );
             oUl.onclick = function (ev){
                  var ev = ev || window.event;
                      var target = ev.target || ev.srcElement;
                  if (target.nodeName.toLowerCase() == 'li' ){
                         alert(123);
                 alert(target.innerHTML);
                     }
             }
         }

這樣改下就只有點擊li會觸發事件了,且每次只執行一次dom操作,如果li數量很多的話,將大大減少dom的操作,優化的性能可想而知!

上面的例子是說li操作的是同樣的效果,要是每個li被點擊的效果都不一樣,那么用事件委托還有用嗎?

?
1
2
3
4
5
6
< div id = "box" >
     < input type = "button" id = "add" value = "添加" />
     < input type = "button" id = "remove" value = "刪除" />
     < input type = "button" id = "move" value = "移動" />
     < input type = "button" id = "select" value = "選擇" />
   </ div >
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
window.onload = function (){
       var Add = document.getElementById( "add" );
       var Remove = document.getElementById( "remove" );
       var Move = document.getElementById( "move" );
       var Select = document.getElementById( "select" );
       
       Add.onclick = function (){
         alert( '添加' );
       };
       Remove.onclick = function (){
         alert( '刪除' );
       };
       Move.onclick = function (){
         alert( '移動' );
       };
       Select.onclick = function (){
         alert( '選擇' );
       }
       
     }

上面實現的效果我就不多說了,很簡單,4個按鈕,點擊每一個做不同的操作,那么至少需要4次dom操作,如果用事件委托,能進行優化嗎?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
window.onload = function (){
       var oBox = document.getElementById( "box" );
       oBox.onclick = function (ev) {
         var ev = ev || window.event;
         var target = ev.target || ev.srcElement;
         if (target.nodeName.toLocaleLowerCase() == 'input' ){
           switch (target.id){
             case 'add' :
               alert( '添加' );
               break ;
             case 'remove' :
               alert( '刪除' );
               break ;
             case 'move' :
               alert( '移動' );
               break ;
             case 'select' :
               alert( '選擇' );
               break ;
           }
         }
       }
       
     }

用事件委托就可以只用一次dom操作就能完成所有的效果,比上面的性能肯定是要好一些的

現在講的都是document加載完成的現有dom節點下的操作,那么如果是新增的節點,新增的節點會有事件嗎?也就是說,一個新員工來了,他能收到快遞嗎?

看一下正常的添加節點的方法:

?
1
2
3
4
5
6
7
< input type = "button" name = "" id = "btn" value = "添加" />
   < ul id = "ul1" >
     < li >111</ li >
     < li >222</ li >
     < li >333</ li >
     < li >444</ li >
   </ ul >

現在是移入li,li變紅,移出li,li變白,這么一個效果,然后點擊按鈕,可以向ul中添加一個li子節點

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
window.onload = function (){
       var oBtn = document.getElementById( "btn" );
       var oUl = document.getElementById( "ul1" );
       var aLi = oUl.getElementsByTagName( 'li' );
       var num = 4;
       
       //鼠標移入變紅,移出變白
       for ( var i=0; i<aLi.length;i++){
         aLi[i].onmouseover = function (){
           this .style.background = 'red' ;
         };
         aLi[i].onmouseout = function (){
           this .style.background = '#fff' ;
         }
       }
       //添加新節點
       oBtn.onclick = function (){
         num++;
         var oLi = document.createElement( 'li' );
         oLi.innerHTML = 111*num;
         oUl.appendChild(oLi);
       };
     }

這是一般的做法,但是你會發現,新增的li是沒有事件的,說明添加子節點的時候,事件沒有一起添加進去,這不是我們想要的結果,那怎么做呢?一般的解決方案會是這樣,將for循環用一個函數包起來,命名為mHover,如下:

?
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
window.onload = function (){
       var oBtn = document.getElementById( "btn" );
       var oUl = document.getElementById( "ul1" );
       var aLi = oUl.getElementsByTagName( 'li' );
       var num = 4;
       
       function mHover () {
         //鼠標移入變紅,移出變白
         for ( var i=0; i<aLi.length;i++){
           aLi[i].onmouseover = function (){
             this .style.background = 'red' ;
           };
           aLi[i].onmouseout = function (){
             this .style.background = '#fff' ;
           }
         }
       }
       mHover ();
       //添加新節點
       oBtn.onclick = function (){
         num++;
         var oLi = document.createElement( 'li' );
         oLi.innerHTML = 111*num;
         oUl.appendChild(oLi);
         mHover ();
       };
     }

雖然功能實現了,看着還挺好,但實際上無疑是又增加了一個dom操作,在優化性能方面是不可取的,那么有事件委托的方式,能做到優化嗎?

?
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
30
31
32
window.onload = function (){
       var oBtn = document.getElementById( "btn" );
       var oUl = document.getElementById( "ul1" );
       var aLi = oUl.getElementsByTagName( 'li' );
       var num = 4;
       
       //事件委托,添加的子元素也有事件
       oUl.onmouseover = function (ev){
         var ev = ev || window.event;
         var target = ev.target || ev.srcElement;
         if (target.nodeName.toLowerCase() == 'li' ){
           target.style.background = "red" ;
         }
         
       };
       oUl.onmouseout = function (ev){
         var ev = ev || window.event;
         var target = ev.target || ev.srcElement;
         if (target.nodeName.toLowerCase() == 'li' ){
           target.style.background = "#fff" ;
         }
         
       };
       
       //添加新節點
       oBtn.onclick = function (){
         num++;
         var oLi = document.createElement( 'li' );
         oLi.innerHTML = 111*num;
         oUl.appendChild(oLi);
       };
     }

看,上面是用事件委托的方式,新添加的子元素是帶有事件效果的,我們可以發現,當用事件委托的時候,根本就不需要去遍歷元素的子節點,只需要給父級元素添加事件就好了,其他的都是在js里面的執行,這樣可以大大的減少dom操作,這才是事件委托的精髓所在。

--------------------------------------------------華麗的分割線-------------- -----------------------------------------------------------------------------------------------------

在這里先感謝一下@蒼茫大地NV 的提問,提的問題非常好!👏👏👏

他的問題是:

現在給一個場景 ul > li > div > p,div占滿li,p占滿div,還是給ul綁定時間,需要判斷點擊的是不是li(假設li里面的結構是不固定的),那么e.target就可能是p,也有可能是div,這種情況你會怎么處理呢?

那我們現在就再現一下他給的場景

?
1
2
3
4
5
6
7
8
9
10
11
12
13
< ul id = "test" >
     < li >
       < p >11111111111</ p >
     </ li >
     < li >
       < div >
       </ div >
     </ li >
     < li >
       < span >3333333333</ span >
     </ li >
     < li >4444444</ li >
   </ ul >

如上列表,有4個li,里面的內容各不相同,點擊li,event對象肯定是當前點擊的對象,怎么指定到li上,下面我直接給解決方案:

?
1
2
3
4
5
6
7
8
9
10
11
var oUl = document.getElementById( 'test' );
   oUl.addEventListener( 'click' , function (ev){
     var target = ev.target;
     while (target !== oUl ){
       if (target.tagName.toLowerCase() == 'li' ){
         console.log( 'li click~' );
         break ;
       }
       target = target.parentNode;
     }
   })

核心代碼是while循環部分,實際上就是一個遞歸調用,你也可以寫成一個函數,用遞歸的方法來調用,同時用到冒泡的原理,從里往外冒泡,知道currentTarget為止,當當前的target是li的時候,就可以執行對應的事件了,然后終止循環,恩,沒毛病!

這里看不到效果,大家可以復制過去運行一下!

--------------------------------------------------------------------華麗的分割線----------------------------------------------------------------

總結:

那什么樣的事件可以用事件委托,什么樣的事件不可以用呢?

適合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。

值得注意的是,mouseover和mouseout雖然也有事件冒泡,但是處理它們的時候需要特別的注意,因為需要經常計算它們的位置,處理起來不太容易。

不適合的就有很多了,舉個例子,mousemove,每次都要計算它的位置,非常不好把控,在不如說focus,blur之類的,本身就沒用冒泡的特性,自然就不能用事件委托了。


免責聲明!

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



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