js基礎之事件捕獲與冒泡原理

想要了解什麼是事件捕獲與冒泡,需要先了解什麼是事件。

什麼是事件?

我們知道,在前端開發中,JavaScript負責定義網頁的“行爲”。這裏所說的“定義”,其實指的是開發者可以通過JavaScript語言向瀏覽器描述一些規則,瀏覽器按照這些規則與用戶進行交互。比如開發者希望當用戶點擊頁面上某個按鈕的時候,就彈出一個窗口,顯示特定的內容。而當用戶真正點擊這個按鈕的時候,瀏覽器將按照開發者定義的這個規則,去彈出指定的窗口,顯示指定的內容。

在上面的例子中,瀏覽器是一切規則的執行者,開發者是這些規則的制定者,而JavaScript只是開發者向瀏覽器描述這些規則時所使用的的語言(否則瀏覽器無法知道開發者想要在什麼情況下做什麼事)。假如我們通過以下的語句向瀏覽器描述了一條規則:

<body>
  <button id="btn">點擊</button>
  <script>
    var button = document.getElementById("btn");  //獲取頁面上的按鈕
    button.addEventListener("click", function(){  //定義點擊事件
      alert("我被點擊了");
    })
  </script>
</body>

在這裏插入圖片描述
頁面上現在有一個按鈕,我們首先使用原生DOM獲取這個按鈕,然後使用button.addEventListener(“click”, function(){})這樣的語法向瀏覽器描述了一條規則:當這個按鈕被點擊(click)時,彈出提示框,顯示“我被點擊了”。用戶點擊按鈕後網頁就會出現如下提示:
在這裏插入圖片描述
瀏覽器把這次“點擊”稱爲一個“事件”。“事件”用於描述交互過程中某些特定的關鍵點(如點擊、鼠標滑動、滾輪滾動、按下鍵盤、觸屏操作等,每個操作都對應特定的事件,不過事件也可能與用戶行爲無關,比如網頁加載完畢也是一個事件)。而瀏覽器處理交互最重要的手段就是基於事件來執行開發者定義好的回調函數(如在用戶“點擊按鈕”時“彈出窗口”,而定義“彈出窗口”行爲的就是回調函數,也就是addEventListener中的function)。

定義完這條規則,當用戶點擊按鈕時,瀏覽器就會彈出上述窗口了。我們稱“點擊”這個事件是在這個按鈕上觸發的(因爲我們的回調函數是綁定在這個按鈕上的)。

那什麼是事件的捕獲與冒泡呢?

事件的捕獲與冒泡

這個問題與HTML的結構息息相關。

在前端開發中,我們使用標籤語言HTML來描述網頁結構,如一個標題、一個段落、一個表格等,這些網頁元素描述了網頁上有哪些需要顯示的內容,它們構成了整個網頁的“骨骼”,通常是一種嵌套的結構,比如:

<html>
    <head>
        ...  //這是對網頁內容的元描述
    </head>
    
    <body>   //這是網頁需要渲染的真正內容
        <div>
            <h1>標題</h1>
            <p>這裏是一個段落</p>
        </div>
    </body>
</html>

上述網頁結構示意圖如下(在沒有設置padding等屬性的情況下,子元素通常會填滿父元素,這裏的內間距只是爲了說明元素的嵌套關係):
在這裏插入圖片描述
我們看到,body元素是整個網頁的容器,它的內部包含了一個div元素,而div的內部又包含了兩個元素:h1和p。假如我們現在在p的內部點擊了一下,那麼請問我們有沒有點擊它的外部容器div,以及最外部的body呢?

從瀏覽器的角度來看,我們同時在點擊這三個元素。

想要證明這個結論非常簡單,只需要使用addEventListener向div和body各自綁定click事件,如果點擊p時也會被觸發,那就說明上面的結論是正確的。毫無疑問,它們會被觸發。

那麼問題來了,既然用戶同時在點擊這三個元素,瀏覽器應該先執行哪個元素定義的回調函數呢(由於JavaScript採用單線程模型,執行回調函數必然有一定的先後順序)?

這個問題實際上是在說,對於嵌套的元素,應該從內向外還是從外向內響應事件。瀏覽器之爭的兩大對立方分別有自己的看法:Netscape公司認爲應當由最外層的body首先得到這個事件,其次是div,最後纔是目標元素p;而微軟的IE開發組則認爲,應當是內部的p首先得到這個事件,然後是div,最後纔是body。在沒有標準約束的情況下,兩者按照自己的想法去設計瀏覽器的事件模型,Netscape從外向內傳播的模型在業內被稱爲事件捕獲模型,而微軟從內向外傳播的模型則被稱爲事件冒泡模型。

兩個模型雖然從思路上南轅北轍,但是都可以保證所有綁定的回調函數正確觸發(不過觸發順序是相反的。如果這個觸發順序很重要,那麼在當時,你的代碼可能只能在一個瀏覽器中正確運行,或者去做噁心的瀏覽器兼容)。不過瀏覽器允許開發者在事件傳播的過程中阻止事件的繼續傳播,此時兩者的差異就變得極其明顯。

假如我們在定義點擊div元素的回調函數時阻止了事件的傳播:

div.addEventListener("click", function(e){
  ...
  e.stopPropagation();  //阻止事件繼續傳播
})

這個代碼會在兩種模型下產生巨大的差異。在捕獲模型中,由於最外部首先得到該事件,因此body的點擊事件首先被觸發,之後是div的點擊事件。由於阻止了事件傳播,p元素不會觸發回調。而在冒泡模型中則恰恰相反,內部的p首先得到該事件,其次纔是div,因此觸發回調的將是p和div,body因爲事件沒有冒泡上來而無法監聽到該事件。同樣的代碼在兩種模型中產生了完全不同的行爲,這對於開發者來說顯然是不可接受的(兩個模型都有自己的適用場景,也都有自己的合理性,因此對於模型的好壞不能一概而論)。

那麼後來的國際標準組織是如何解決這個衝突的呢?答案就是由開發者自己選擇。

標準的事件綁定使用addEventListener函數,它接收兩個必傳參數和一個可選參數:必傳的爲event(事件名,如"cick")和function(回調函數),可選的爲useCapture(是否使用捕獲模型,默認爲false,根據MDN的接口說明,這裏也可以傳入一個對象,爲本次監聽設置其他參數,詳細請參考MDN接口文檔 - addEventListener)。

div.addEventListener("click", function(){}, true); //使用捕獲模型

第三個參數就是標識開發者是否需要使用捕獲模型,默認爲false,也就是默認使用微軟的冒泡模型(這是因爲大多數事件都只在最內部的元素上觸發,這也間接表明,冒泡模型的普適性更好)。如果開發者的需求確實需要使用捕獲模型,可以將第三個參數設置爲true。比如下面的例子:

事件捕獲與冒泡的用法

瞭解了事件捕獲與冒泡的基本原理之後,我們舉個例子來說明這兩個模型的基本用法。

假設有以下的DOM結構:

<div id="outer">
    <div id="inner"  style="width:100px;height: 100px;border: 1px solid black;">
        
    </div>
 </div>

這是兩個重疊的div,當點擊時,兩者都會響應這個click事件。假如事件綁定如下:

  var outer = document.querySelector("#outer");
  var inner = document.querySelector("#inner");
  outer.addEventListener("click", function(e){
      alert("來自外部div的消息");
      e.stopPropagation();  //阻止事件向內部傳播
  }, true);   //使用捕獲模型

  inner.addEventListener("click", function(e){
      alert("來自內部div的消息");
  }, true);   //使用捕獲模型

頁面上將只顯示外部彈出的消息,內部的事件被e.stopPropagation()攔截了下來,導致事件沒有觸發。而如果寫成下面的代碼:

  var outer = document.querySelector("#outer");
  var inner = document.querySelector("#inner");
  outer.addEventListener("click", function(e){
      alert("來自外部div的消息");
  }, false);   //使用冒泡模型

  inner.addEventListener("click", function(e){
      alert("來自內部div的消息");
      e.stopPropagation();  //阻止事件向外部傳播
  }, false);   //使用冒泡模型

這次是隻顯示了內部的消息,而沒有顯示外部的消息,說明事件在向上冒泡的過程中被阻止了。

注意
如果是在表格中內嵌複選框,希望實現點擊一行時選中複選框,通過stopPropagation阻止CheckBox響應click事件並不能實現。測試發現複選框狀態改變的事件似乎並不是在click事件觸發的(斷點跟蹤表明,CheckBox在執行click回調之前,狀態就已經發生了改變,具體是通過什麼事件改變了選中狀態尚不清楚),下面給一個可以處理行點擊的示例:

<table border="1" cellspacing="0">
  <tr class="tr">
    <td>
      <input class="checkbox" type="checkbox">
     
    </td>
    <td>
      表格第一行
    </td>
  </tr>
  <tr class="tr">
    <td>
      <input class="checkbox" type="checkbox">
    </td>
    <td>
      表格第二行
    </td>
  </tr>
</table>
<script>
  var tr = document.querySelectorAll(".tr"); //獲取所有tr
  tr.forEach(function(item){  //爲每個tr綁定click事件,手動選中複選框
    item.addEventListener("click", function(e){
      var checkbox = item.querySelector(".checkbox");
      checkbox.checked = !checkbox.checked;
    })
  })

  var cb = document.querySelectorAll(".checkbox");
  cb.forEach(function(item){
    item.addEventListener("click", function(e){
      this.checked = !this.checked;
    });
  })
</script>

這裏沒有使用stopPropagation阻止事件傳播,而是通過爲CheckBox定義額外的click事件來解決狀態不變的問題(經過斷點跟蹤,此時在點擊CheckBox時,狀態發生了三次變化,第一次是觸發了某個原生事件導致其狀態變化,第二次是執行了tr的點擊事件,第三次則是爲CheckBox自定義的click事件)。也就是說,點擊tr時狀態改變一次,點擊CheckBox時狀態改變三次,功能均正常。

由於在大多數情況下,事件都是由最內層的元素來處理的,所以冒泡模型的應用更爲廣泛,它也因此成爲綁定事件時使用的默認模型。

總結

事件的捕獲與冒泡兩個模型相對比較簡單,只要明白了其中的原理,就可以很容易掌握通過stopPropagation阻止事件傳播的使用。

瀏覽器的標準事件模型把事件的傳播過程分成了三個階段:捕獲階段、處於目標階段和冒泡階段。捕獲階段指事件從最外層傳播到最內層之前的整個過程,對應捕獲模型;處於目標階段指的是事件剛好傳播到目標元素上;而冒泡階段指的是從最內層元素向外傳播的整個過程。所以我們看到,標準的瀏覽器事件模型就是把捕獲模型和冒泡模型有機地結合起來,使開發者可以以最簡單的方式靈活地使用兩個模型。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章