[HTML BTS] 冒泡? 捕捉?Capturing & Bubbling in DOM Event


Posted by Powerfultraveling 's Blog on 2021-07-03

前言

一開始學到這一部分的時候,因為做的作品都很小,沒有用到這一部分的觀念,所以很快速的就帶了過去,但隨著開發經驗增加,發現機制的廣泛性,所以回來從新學了一次。記下這個筆記,梳理自己的邏輯。

這個理論要解決甚麼問題?

所有的理論都是起源自某個問題,在學習理論之前,必須要先了解它要解決甚麼問題,比較容易參透,所以我們就先從面對問題開始吧:

HTML 的元素都是由母元素包住子元素,層層交疊,在畫面上的結構就像下面的範例:

  • 簡化版程式碼
    <html>
    <head>
    </head>
    <body>
      <div class="Element1">
        <div class="Element2">
          <div class="Element3">
          </div>
        </div>
      </div>
    </body>
    </html>
    

所以呢,假如我點按了最內層的 Element3 就等於同時點按了包住 Element3 的上層元素 Element2 和 Element1。(這邊可以將 Elemnet3 想像成家裡鋪好的木頭地板,Element2 和 Element1 則是底下的水泥層和泥土層,當你踩上木頭地板時,你也踩上了底下的水泥層和泥土層。)

有了這樣的概念後,我們進一步看這樣的原理會造成甚麼問題吧!

假如我在 Element3、Element2 以及 Element1 都設置了一個監聽 click 事件的 evnetListner。在這樣的設置下,當我點按 Element3 時等於三個元素同時都被點按到,而這三個 Element都有設置 eventListener,究竟哪一個 Element 的 event handler 可以優先被啟動呢?這也是bubbling 和 capturing 所要解決的主要問題:「執行順序」。

解決方式

解決執行順序問題的兩種方式

在古代,對於這個問題的解決方式因為瀏覽器以及公司不同,而有不同的排序方式:

capturing:

根據這個規則,瀏覽器會從根元素開始,一層層檢查是否有元素裝有 event handler,如果有的話就執行該 event handler,沒有的話就繼續往下找,直到到真正被點到的元素為止。依照這個規則,假如我點按了 <div class="Element3"> 跑起來的實際順序會是長這樣:

Window > Document > html > body > div(".element1") > div(".element2") > div(".element3")

換言之,Element1 的事件會先被觸發,再來是 Element2 最後才是 Element3。

bubbling:

根據這個規則,瀏覽器為先從被實際點按的元素開始往上一層層檢查,是否有任何的元素有裝設該事件的 event handler,有的話就執行,沒有的話就繼續往上找,直到根元素為止。依照這個規則,跑起來的實際順序會是長這樣:

div(".element3") > div(".element2") > div(".element3") > body > html > Document > Window

換言之,Element3 的事件會先被觸發,再來是 Element2 最後才是 Element1。

W3C規範

把兩個組起來吧!

因為沒有統一的標準,各家瀏覽器各自選擇採納不同的標準,造成開發上很多的小問題,因此,W3C 最後制定了一個通用的標準: 大家別再吵了,我們 capturing 和 bubbling 通通都要可以用,由 developer 自行決定要用哪個吧!

組起來後怎麼跑?

其實這個新規則很單純,就是將已存在的兩種規則 capturing 以及 bubbling 組裝在一起,拆成 capturing phase 、target phase、bubbling phase,三個階段。整個流程會長成這樣:

  1. 依照capturing 的方式往下傳遞(capturing phase)
  2. 停留在實際被點擊的目標元素(target phase)
  3. bubbling 的方式往上傳遞(bubbling phase)

以這張圖為例,當目標元素 td 被點擊後:

  1. 瀏覽器會先依 capturing 的規則,從根元素開始一層層往下找。
  2. 直到實際被點擊的目標元素(在這張圖的例子裏就是td),停留。
  3. 再依照 bubbling 的規則從目標元素 td 往上一層層找直到根元素。

這個機制造成的影響

再了解完整個大架構大概是怎麼跑之後,let's work on 在這個大架構底下的重要觀念!

addEventListener 與 capturing、bubbling

從 W3C 制定了標準之後,developer 可以自由決定 element 的 event handler 要在 capturing 的階段還是 bubbling 的階段啟動。話雖如此,在實務上究竟要怎麼做呢?在這邊以原生的 DOM 方法做實際演練:

這是我們常用的 addEventListener()

 e1ement.addEventListener("click", function(){
        console.log("Guten Tag");
    });

實際上,除了第一個參數事件類型、第二個參數 event handler以外,其實還有第三個參數可以傳入。
第三個參數是一個 Boolean ,true 代表 event handler要在 capturing 的階段被啟動;而 false 則代表 event handler 要在 bubbling 的階段被啟動。

 e1ement.addEventListener("click", function(){
        console.log("element1")
    }, true);

這時你可能會好奇,那之前沒有特別設定時,就沒有 capturing 和 bubbling 了嗎?

答案是否定的 !

在 DOM 事件的世界裡,只要有事件的發生,capturing 和 bubbling 就會發生。所以呢,假如在 addEventListener 裡沒有特別設定,它也會預設為 bubbling。

1. 用 addEventListener() 來看看整個機制吧

首先,先寫一個簡單的 HTML:

<html>
  <head>
  </head>
  <body>
    <div class="Element1">
      <div class="Element2">
        <div class="Element3">
        </div>
      </div>
    </div>
  </body>
</html>

畫面結構大概長這樣:

再來,幫每個元素加上 event listener:

 <script>
    let e1 = document.querySelector(".Element1");
    let e2 = document.querySelector(".Element2");
    let e3 = document.querySelector(".Element3");

    e1.addEventListener("click", function(){
        console.log("element1 capturing")
    }, true);

    e1.addEventListener("click", function(){
        console.log("element1 bubbling")
    }, false);


    e2.addEventListener("click", function(){
        console.log("element2 capturing")
    }, true);

    e2.addEventListener("click", function(){
        console.log("element2 bubbling")
    }, false);


    e3.addEventListener("click", function(){
        console.log("element3 capturing")
    }, true);

    e3.addEventListener("click", function(){
        console.log("element3 bubbling")
    }, false);

</script>

在我們 click element3 後會依序跑出:

  • element1 capturing
  • element2 capturing
  • element3 capturing
  • element3 bubbling
  • element2 bubbling
  • element1 bubbling

因為不管如何都會按照 capturing 和 bubbling 的機制去跑,所以呢就算調換程式碼的排列順序,最後還是會依照上面的順序一個個被觸發並跑出結果。
但這邊有個例外,假如更改的是實際被點按的目標元素,得出來的結果會不一樣,那是因為,當這個機制跑到目標元素時等於進入了 target phase,而在 target phase 並沒有分 capturing 和 bubbling。因此,如果調換這邊的程式碼,結果是會改變的!

根據以上的行為在可以做個小總結:

  1. 這個機制會先捕獲再冒泡。
  2. 當傳遞到實際被點按的 target element 時,就等於跑到了 target phase,既不屬於 capturing 也不屬於 bubbling。

2. 怎麼中止事件的傳遞

stopPropagation()

addEventListener 於事件被觸發時會產生一筆有關於此事件的資料,並且可以當作參數傳進 event handler,以供利用。(可以把 function 想像成實際到現場執行的基層人員,總要有相關資料才知道怎麼做吧)。
這個參數不單提供資訊,也提供了一些方法,例如我們可以藉這個參數使用 stopPropagation(),來停止這個事件的傳遞。

element.addEventListener("click", function(e){
  e.stopPropagation()
})

所以在 bubbling and capturing 在實作上有甚麼用呢?

事件代理(event delegation)

事件代理的原理很單純,在前面我們了解到,不管我們點按甚麼元素到最後一定都會 bubbling 回去 parent element, 因為這樣的機制存在,我們可以利用 parent element 來代理接收事件、並處理相對應的 event handler,單用文字可能不好理解,接下來用以下的例子來時作一次,會更容易清楚:

假如現在我有一個 ul element,裡面放三個 li element,並且之後可能會隨著需求增加 il element 的數量:

    <ul class="list">
        <li class="list_item">1</li>
        <li class="list_item2">2</li>
        <li class="list_item3">3</li>
    </ul>

除此之外,我希望在點擊各個 li element 的時候,都會 log 出各 li element 裡的 text,這時候我們可以在一個一個幫 li element 加上 EventListner 來監聽事件:

    <script>
        let list_item
        =  document.querySelector(".list_item");

        list_item.addEventListener(
        "click", 
        function(){
            let content = list_ite,.textContent;
            console.log(content);
        })
    </script>

但是如果 li element 很多的話,這樣手動一個個加上去,其實會花上許多的時間,因此,在這裡我們可以利用 capturing 以及 bubbling 的特性,用 ul 來監聽事件,這樣不管加幾個 li,因為他們始終都是被身為 parent element 的 ul 所包住的,所以在 每個 li 上面所發生的事件 ul 也同樣可以接收的到:

利用這樣的特性,再加上 addEventListener 監聽事件所製成,並且傳進 event handler 當參數的 event data,我們就可以把 event listener 裝在 parent element,再由 event handler 去判斷要怎麼執行:

    <script>
        let list = document.querySelector(".list");
        list.addEventListener("click", function(e){
            let target = e.target;//找到被點按的那個 li 
            let text = target.textContent;
            console.log(text);
        })
    </script>

這樣做的話就不需要在一個個幫每個 li 裝上 event listener,不單省下許多功夫,也讓整個程式碼的編排更 dry、更易讀!

總結

這篇是我第一篇詳實紀錄的筆記,希望跟我一樣的初學者,在看這篇文章後會更容易理解 capturing 與 bubbling 的機制。
當然,畢竟是一篇初學者所寫的文章,如果有任何不足的地方,也請讀者不另指教!

參考資料

quirksmode org
huli DOM 的事件傳遞機制:捕獲與冒泡
MDN










Related Posts

CH5 IF, else 條件判斷入門

CH5 IF, else 條件判斷入門

深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?

深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?

W12_作業一實作記錄 [ MTR05 ] 實作之三

W12_作業一實作記錄 [ MTR05 ] 實作之三


Comments