學習目標:了解 DOM 裡傳遞的順序以及什麼是「事件代理」
本文主要源自以下資料的學習整理:
在 [第八週]DOM — 瀏覽器事件處理 了解到在瀏覽器中各種偵測事件的方法後,要再深入了解事件在 DOM 裡傳遞的順序
以下簡單範例說明當我們使用 addEventListener 時事件傳遞的順序
如果點擊 outer 會觸發 outer
如果點擊 inner 會觸發 inner ➡️ outer
如果點擊 button 會觸發 button ➡️ inner ➡️ outer
由此可知當點擊內部節點,同時也會點擊到外層節點。
捕獲與冒泡
實際上事件的觸發分為三個階段,我們可以利用事件資訊裡面的 eventPhase
,知道事件是在哪一個階段觸發。
數字 1 :捕捉 CAPTURING_PHASE
數字 2 :目標 AT_TARGET
數字 3 :冒泡 BUBBLING_PHASE
console.log(e.eventPhase)
所以我們可以知道當點擊 .btn
時觸發順序時從目標階段(.btn) ➡️ 冒泡階段(.inner、.outer)
咦,那「捕捉階段」是在什麼時候發生呢?
我們可以從 w3c 講 event flow 的圖 來更清楚的了解
DOM 的事件在傳遞時,會先從根節點開始往下傳遞到 target,如果在這邊加上事件的話,傳遞順序就是:捕獲階段(.btn) ➡️ 目標階段。表示我要在「捕獲階段」去監聽事件。
而如果是從子節點一路逆向傳回去跟節點,在這邊加上事件的話,傳遞順序就是:目標階段 ➡️ 冒泡階段。表示我要在「冒泡階段」去監聽事件。#就是上面的範例。
這就是一般看到事件機制的文章的時候,都會看到一個口訣:
先捕獲,再冒泡
要如何決定在捕獲階段還是冒泡階段去監聽這個事件?
.addEventListener 其實還有第三個參數 useCapture
- 布林值,指定事件在捕獲或冒泡階段執行
1. true 補獲
2. false 冒泡(默認值)
我們添加第三個參數 log 出來看看
我們可以認真看一下 .btn 上 eventPhase
數字皆為 2 代表他們是同階級 AT_TARGET ,因此會依照 addEventListener 的順序而定(上面的 code 是先寫冒泡在寫捕獲)
事件傳遞兩個原則
- 先捕獲,再冒泡
- 當事件傳到 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
冒泡。- 事件傳遞兩個原則
- 先捕獲,在冒泡
- 當事件傳到
target
本身,沒有分捕獲與冒泡
e.stopPropagation
停止傳遞- 同個元素可以有多個監聽事件
e.stopImmediatePropagation
除了停止傳遞,也阻止同元素其他相同的監聽事件。
以上有錯誤的地方歡迎指正,感謝。