事件冒泡、事件捕獲和事件委託

摘自: https://www.cnblogs.com/Chen-XiaoJun/p/6210987.htmljavascript

事件流css

  JavaScript與HTML之間的交互是經過事件實現的。事件,就是文檔或瀏覽器窗口中發生的一些特定的交互瞬間。可使用偵聽器來預訂事件,以便事件發生時執行相應的代碼。
  事件流的起源:就是在瀏覽器發展到第四代的時候,瀏覽器開發團隊遇到一個問題:頁面的哪一部分會擁有某個特定的事件?要明白這個問題問的是什麼,能夠想象畫在一張紙上的一組同心圓。若是你把手指放在圓心上,那麼你的手指指向的不是一個圓,而是紙上的全部圓。也就是說若是單擊了頁面的某個按鈕,同時也單擊了按鈕的容器元素,甚至單擊了整個頁面。不過呢,IE提出的是冒泡流,而網景提出的是捕獲流。html

示例:java

 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>事件流</title>
    <style type="text/css">
        #content{width: 150px;height: 150px;background-color: red;}
        #btn{width: 80px;height: 80px;background-color: green;}
    </style>
</head>
<body>
    <div id="content">content
        <div id="btn">button</div>
    </div>

    <script type="text/javascript">
        var content = document.getElementById("content");
        var btn = document.getElementById('btn');
        btn.onclick = function(){
            alert("btn");
        };
        content.onclick = function(){
            alert("content");
        };
        document.onclick = function(){
            alert("document");
        }
    </script>
</body>
</html>
 

  若是點擊容器#btn,則彈出的順序是:btn-content-document;若是點擊的是容器#content,則彈出的是content-document;若是點擊的是document,彈出的是document。node

  由此能夠看出JavaScript的事件流機制瀏覽器

  前面說過,IE提出的是冒泡流,而網景提出的是捕獲流,後來在W3C組織的統一之下,JS支持了冒泡流和捕獲流,可是目前低版本的IE瀏覽器仍是隻能支持冒泡流(IE6,IE7,IE8均只支持冒泡流),因此爲了可以兼容更多的瀏覽器,建議你們使用冒泡流。app

  JS事件流原理圖以下:函數

  

由此能夠知道
  一、一個完整的JS事件流是從window開始,最後回到window的一個過程
  二、事件流被分爲三個階段(1~5)捕獲過程、(5~6)目標過程、(6~10)冒泡過程性能

示例:測試

 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style type="text/css">
    #wrapDiv, #innerP, #textSpan{
        margin: 5px;padding: 5px;box-sizing: border-box;cursor: default;
    }
    #wrapDiv{
        width: 300px;height: 300px;border: indianred 3px solid;
    }
    #innerP{
        width: 200px;height: 200px;border: hotpink 3px solid;
    }
    #textSpan{
        display: block;width: 100px;height: 100px;border: orange 3px solid;
    }
    </style>
</head>
<body>
     <div id="wrapDiv">wrapDiv
        <p id="innerP">innerP
            <span id="textSpan">textSpan</span>
        </p>
    </div>
    <script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 捕獲階段綁定事件
    window.addEventListener("click", function(e){
        console.log("window 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡階段綁定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>
</body>
</html>
 

  這個時候,若是點擊一下textSpan這個元素,控制檯會打印出這樣的內容:

從上面所畫的事件傳播的過程可以看出來,當點擊鼠標後,會先發生事件的捕獲

  · 捕獲階段:首先window會獲捕獲到事件,以後document、documentElement、body會捕獲到,再以後就是在body中DOM元素一層一層的捕獲到事件,有wrapDiv、innerP。

  · 目標階段:真正點擊的元素textSpan的事件發生了兩次,由於在上面的JavaScript代碼中,textSapn既在捕獲階段綁定了事件,又在冒泡階段綁定了事件,因此發生了兩次。可是這裏有一點是須要注意,在目標階段並不必定先發生在捕獲階段所綁定的事件,而是先綁定的事件發生,一會會解釋一下。

  · 冒泡階段:會和捕獲階段相反的步驟將事件一步一步的冒泡到window

上述代碼中的兩個屬性:e.target和e.currentTarget

  target和currentTarget都是event上面的屬性,target是真正發生事件的DOM元素,而currentTarget是當前事件發生在哪一個DOM元素上。

  能夠結合控制檯打印出來的信息理解下,目標階段也就是 target == currentTarget的時候。我沒有打印它們兩個由於太長了,因此打印了它們的nodeName,可是因爲window沒有nodeName這個屬性,因此是undefined。

 

  那可能有一個疑問,咱們不用addEventListener綁定的事件會發生在哪一個階段呢,咱們來一個測試,順便再演示一下我在上面的目標階段所說的目標階段並不必定先發生捕獲階段所綁定的事件是怎麼一回事。

 
<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 測試直接綁定的事件到底發生在哪一個階段
    wrapDiv.onclick = function(){
        console.log("wrapDiv onclick 測試直接綁定的事件到底發生在哪一個階段")
    };

    // 捕獲階段綁定事件
    window.addEventListener("click", function(e){
        console.log("window 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(){
        console.log("textSpan 冒泡 在捕獲以前綁定的")
    }, false);

    textSpan.onclick = function(){
        console.log("textSpan onclick")
    };

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡階段綁定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>
 

  控制檯打印以下:

  

  · textSpan是被點擊的元素,也就是目標元素,全部在textSpan上綁定的事件都會發生在目標階段,在綁定捕獲代碼以前寫了綁定的冒泡階段的代碼,因此在目標元素上就不會遵照先發生捕獲後發生冒泡這一規則,而是先綁定的事件先發生。[在目標元素上就不會遵照先發生捕獲後發生冒泡這一規則,而是先綁定的事件先發生。]
  · 因爲wrapDiv不是目標元素,因此它上面綁定的事件會遵照先發生捕獲後發生冒泡的規則。因此很明顯用onclick直接綁定的事件發生在了冒泡階段。

說一下事件綁定、解綁還有阻止事件默認行爲:

事件綁定:

  一、直接獲取元素綁定:

element.onclick = function(e){
        // ...
    };

  該方法的優勢是:簡單和穩定,能夠確保它在你使用的不一樣瀏覽器中運做一致;處理事件時,this關鍵字引用的是當前元素,這頗有幫組。

  缺點:只會在事件冒泡中運行;一個元素一次只能綁定一個事件處理函數,新綁定的事件處理函數會覆蓋舊的事件處理函數;事件對象參數(e)僅非IE瀏覽器可用

  二、直接在元素裏面使用事件屬性

  三、W3C方法:

element.addEventListener('click', function(e){
        // ...
    }, false);

  優勢:該方法同時支持事件處理的捕獲和冒泡階段;事件階段取決於addEventListener最後的參數設置:false (冒泡) 或 true (捕獲);在事件處理函數內部,this關鍵字引用當前元素;事件對象老是能夠經過處理函數的第一個參數(e)捕獲;能夠爲同一個元素綁定你所但願的多個事件,同時並不會覆蓋先前綁定的事件

  缺點:IE不支持,你必須使用IE的attachEvent函數替代。

  IE下的方法:

element.attachEvent('onclick', function(){
        // ...
});

  優勢:能夠爲同一個元素綁定你所但願的多個事件,同時並不會覆蓋先前綁定的事件。
  缺點:IE僅支持事件捕獲的冒泡階段;事件監聽函數內的this關鍵字指向了window對象,而不是當前元素(IE的一個巨大缺點);事件對象僅存在與window.event參數中;事件必須以ontype的形式命名,好比,onclick而非click;僅IE可用,你必須在非IE瀏覽器中使用W3C的addEventListener

  注意:不是意味這低版本的ie沒有事件捕獲,它也是先發生事件捕獲,再發生事件冒泡,只不過這個過程沒法經過程序控制。

解除事件:

element.removeEventListener('click', function(e){
        // ...
    }, false);

IE:

element.detachEvent('onclick', function(){
        // ...
});

阻止事件傳播

  在支持addEventListener()的瀏覽器中,能夠調用事件對象的stopPropagation()方法以阻止事件的繼續傳播。若是在同一對象上定義了其餘處理程序,剩下的處理程序將依舊被調用,但調用stopPropagation()以後任何其餘對象上的事件處理程序將不會被調用。不只能夠阻止事件在冒泡階段的傳播,還能阻止事件在捕獲階段的傳播。

  IE9以前的IE不支持stopPropagation()方法,而是設置事件對象cancelBubble屬性爲true來實現阻止事件進一步傳播。

 
<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 測試直接綁定的事件到底發生在哪一個階段
    wrapDiv.onclick = function(){
        console.log("wrapDiv onclick 測試直接綁定的事件到底發生在哪一個階段")
    };

    // 捕獲階段綁定事件
    window.addEventListener("click", function(e){
        console.log("window 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕獲", e.target.nodeName, e.currentTarget.nodeName);
        // 在捕獲階段阻止事件的傳播
        e.stopPropagation();
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(){
        console.log("textSpan 冒泡 在捕獲以前綁定的")
    }, false);

    textSpan.onclick = function(){
        console.log("textSpan onclick")
    };

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕獲", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡階段綁定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);
</script>
 

  

  實際上咱們點擊的是textSpan,可是因爲在捕獲階段事件就被阻止了傳播,因此在textSpan上綁定的事件根本就沒有發生,冒泡階段綁定的事件天然也不會發生,由於阻止事件在捕獲階段傳播的特性,e.stopPropagation()不多用到在捕獲階段去阻止事件的傳播,你們就覺得e.stopPropagation()只能阻止事件在冒泡階段傳播。

阻止事件的默認行爲

  e.preventDefault()能夠阻止事件的默認行爲發生,默認行爲是指:點擊a標籤就轉跳到其餘頁面、拖拽一個圖片到瀏覽器會自動打開、點擊表單的提交按鈕會提交表單等等,由於有的時候咱們並不但願發生這些事情,因此須要阻止默認行爲。

  IE9以前的IE中,能夠經過設置事件對象的returnValue屬性爲false達到一樣的效果。

 
function cancelHandler(event){
    var event=event||window.event;//兼容IE
    
    //取消事件相關的默認行爲
    if(event.preventDefault)    //標準技術
        event.preventDefault();
    if(event.returnValue)    //兼容IE9以前的IE
        event.returnValue=false;
    return false;    //用於處理使用對象屬性註冊的處理程序
}
 

 

事件委託:

  在JavaScript中,添加到頁面上的事件處理程序數量將直接關係到頁面的總體運行性能。致使這一問題的緣由是多方面的。首先,每一個函數都是對象,都會佔用內存;內存中的對象越多,性能就越差。其次,必須事先指定全部事件處理程序而致使的DOM訪問次數,會延遲整個頁面的交互就緒時間。

  對「事件處理程序過多」問題的解決方案就是事件委託。事件委託利用了事件冒泡,只指定一個事件處理程序,就能夠管理某一類型的全部事件。例如,click事件會一直冒泡到document層次。也就是說,咱們能夠爲整個頁面指定一個onclick事件處理程序,而沒必要給每一個可單擊的元素分別添加事件處理程序。

 
<ul id="color-list">
    <li>red</li>
    <li>yellow</li>
    <li>blue</li>
    <li>green</li>
    <li>black</li>
    <li>white</li>
</ul>
 

  若是點擊頁面中的li元素,而後輸出li當中的顏色,咱們一般會這樣寫:

 
(function(){
    var color_list = document.getElementById('color-list');
    var colors = color_list.getElementsByTagName('li');
    for(var i=0;i<colors.length;i++){
        colors[i].addEventListener('click',showColor,false);
    };
    function showColor(e){
        var x = e.target;
        alert("The color is " + x.innerHTML);
    };
})();
 

  利用事件流的特性,咱們只綁定一個事件處理函數也能夠完成:

 
(function(){
    var color_list = document.getElementById('color-list');
    color_list.addEventListener('click',showColor,false);
    function showColor(e){
        var x = e.target;
        if(x.nodeName.toLowerCase() === 'li'){
            alert('The color is ' + x.innerHTML);
        }
    }
})();
 

冒泡仍是捕獲?

  對於事件代理來講,在事件捕獲或者事件冒泡階段處理並無明顯的優劣之分,可是因爲事件冒泡的事件流模型被全部主流的瀏覽器兼容,從兼容性角度來講仍是建議你們使用事件冒泡模型。

 

事件委託還有一個好處就是添加進來的元素也能綁定事件:

沒有使用事件委託:

 
<body>
 <ul id="thl">
   <li>001</li>
   <li>002</li>
   <li>003</li>
</ul>
<button onclick="fun()">touch</button>

<script>
    var thl= document.getElementById('thl');
    var aLi = thl.getElementsByTagName('li');
    for (var i = 0; i < aLi.length; i++) {
      aLi[i].onclick = fn;
    }
    
    function fn (){
      console.log(this.innerHTML);
    }

    function fun(){
        var node=document.createElement("li");
        var textnode=document.createTextNode("maomaoliang");
        node.appendChild(textnode);
        document.getElementById("thl").appendChild(node);
    }
</script>
</body>
 

使用了事件委託:

 
<script>
    var thl= document.getElementById('thl');
    thl.onclick = function(ev) {
        ev = ev || event;
        //兼容處理
        var target = ev.target || ev.srcElement;
      //找到li元素
        if (target.nodeName.toLowerCase() == 'li') {
              console.log(target.innerHTML);
         }
    };

    function fun(){
        var node=document.createElement("li");
        var textnode=document.createTextNode("maomaoliang");
        node.appendChild(textnode);
        document.getElementById("thl").appendChild(node);
    }
</script>
相關文章
相關標籤/搜索