[第八週]DOM — 瀏覽器事件傳遞機制

MiaHsu
7 min readApr 21, 2020

--

學習目標:了解 DOM 裡傳遞的順序以及什麼是「事件代理」

本文主要源自以下資料的學習整理:

[第八週]DOM — 瀏覽器事件處理 了解到在瀏覽器中各種偵測事件的方法後,要再深入了解事件在 DOM 裡傳遞的順序

以下簡單範例說明當我們使用 addEventListener 時事件傳遞的順序

將區塊都加上 click 事件,會發現點擊內部區塊,同時也會點擊到外層區塊。

如果點擊 outer 會觸發 outer
如果點擊 inner 會觸發 inner ➡️ outer
如果點擊 button 會觸發 button ➡️ inner ➡️ outer

由此可知當點擊內部節點,同時也會點擊到外層節點。

捕獲與冒泡

實際上事件的觸發分為三個階段,我們可以利用事件資訊裡面的 eventPhase,知道事件是在哪一個階段觸發。

數字 1 :捕捉 CAPTURING_PHASE

數字 2 :目標 AT_TARGET

數字 3 :冒泡 BUBBLING_PHASE

console.log(e.eventPhase)
點擊 .btn console.log 內容

所以我們可以知道當點擊 .btn時觸發順序時從目標階段(.btn) ➡️ 冒泡階段(.inner、.outer)

咦,那「捕捉階段」是在什麼時候發生呢?

我們可以從 w3c 講 event flow 的圖 來更清楚的了解

圖片來源:w3c

DOM 的事件在傳遞時,會先從根節點開始往下傳遞到 target,如果在這邊加上事件的話,傳遞順序就是:捕獲階段(.btn) ➡️ 目標階段。表示我要在「捕獲階段」去監聽事件。

而如果是從子節點一路逆向傳回去跟節點,在這邊加上事件的話,傳遞順序就是:目標階段 ➡️ 冒泡階段。表示我要在「冒泡階段」去監聽事件。#就是上面的範例。

這就是一般看到事件機制的文章的時候,都會看到一個口訣:

先捕獲,再冒泡

要如何決定在捕獲階段還是冒泡階段去監聽這個事件?

.addEventListener 其實還有第三個參數 useCapture

  • 布林值,指定事件在捕獲或冒泡階段執行
    1. true 補獲
    2. false 冒泡(默認值)

我們添加第三個參數 log 出來看看

點擊 .btn console.log 內容怪怪的?不是說「先捕獲,再冒泡」嗎

我們可以認真看一下 .btn 上 eventPhase 數字皆為 2 代表他們是同階級 AT_TARGET ,因此會依照 addEventListener 的順序而定(上面的 code 是先寫冒泡在寫捕獲)

事件傳遞兩個原則

  1. 先捕獲,再冒泡
  2. 當事件傳到 target 本身,沒有分捕獲與冒泡

取消事件傳遞 e.stopPropagation

加在哪邊,事件的傳遞就斷在那裡,不會繼續往下傳遞

document.querySelector('.btn').addEventListener('click',function(e){   e.stopPropagation()   console.log('btn 冒泡');})

同個節點可以有兩個監聽事件

一個節點可以有多個相同監聽事件,請看舉例

document.querySelector('.btn').addEventListener('click',function(e){   console.log('btn1 冒泡');})document.querySelector('.btn').addEventListener('click',function(e){   console.log('btn2 冒泡');})

在 btn1 加上 e.stopPropagation()

想要同一層級的監聽也不要被執行,可改用 e.stopImmediatePropagation()

實際應用

停止頁面上所有元素的預設動作

window.addEventListener('click',function(e){   e.preventDefault() //停止預設動作   e.stopPropagation() //停止後續傳遞},true)//下方事件全部被阻止addEvent('.outer');addEvent('.inner');function addEvent(className){   const element = document.querySelector(className)   element.addEventListener('click',function(e){      console.log(className + ' eventPhase:' + e.eventPhase + ' 冒泡')   },false)}

多個元素綁定 addEventListener

是一個很常碰到的情況,為相似的元素綁定事件。

情境說明:為 class="btn" 的元素在 click 時顯示按鈕上的數字。

直覺反應會使用 for 迴圈來為每個按鈕加上 click 事件

const btn = document.querySelectorAll('.btn')for(var i = 0; i < btn.length; i++){   btn[i].addEventListener('click',function(e){      alert(i+1)   })}

但結果跟我們想的不一樣!每一次都印出 [i]+1 的數字。

原因在 JavaScript 的作用域, 事件是在觸發時才執行,因此在執行之前 for 迴圈早就已經跑完了

解法1. var 改成 let

for(let i = 0; i < btn.length; i++){

動態增加元素、事件代理(event delegation)

解決完多個元素綁定後發現了其他問題:

  • 假設有 100 個元素這樣就要綁定 100 事件,是很沒效率的事情。
  • 如果是後續動態新增的按鈕就無法綁定事件。

因此我們可以運用事件代理(event delegation)來解決

事件代理(event delegation)

我們可以從事件傳遞知道 內元素都會觸發到父元素,因此我們可以直接針對父元素來進行事件監聽,這樣不僅可以大大增加效率,也可以解決動態新增按鈕時無法綁定的問題。

解法:在 html 中用 .btnGroup 將 button 包起並針對 .btns 做監聽事件。

這樣就完成了事件代理

重點整理:

  • 點擊一個元素,同時也會點擊到外層的元素。
  • e.eventPhase 提供觸發事件的階段資訊,分為捕捉、目標、冒泡。
  • addEventListener function 中第三個參數 useCapture 可選擇要在哪個階段觸發事件,true 捕捉、false 冒泡。
  • 事件傳遞兩個原則
  1. 先捕獲,在冒泡
  2. 當事件傳到 target 本身,沒有分捕獲與冒泡
  • e.stopPropagation 停止傳遞
  • 同個元素可以有多個監聽事件
  • e.stopImmediatePropagation 除了停止傳遞,也阻止同元素其他相同的監聽事件。

以上有錯誤的地方歡迎指正,感謝。

--

--

MiaHsu
MiaHsu

Written by MiaHsu

每件事都是最好的安排,成為更好的自己

No responses yet