DOM - 事件傳遞機制
- DOM 的事件在傳播時,會先從根節點開始往下傳遞到 target,再往上從子節點一路逆向傳回去根節點。這段過程中共分為三個階段:
捕獲 => 目標本身 => 冒泡
整個傳遞機制的流程就是:
1.捕獲: - 從根節點開始往下傳遞到 target 的過程
- 以點選
<td>
標籤為例,事件得傳遞可能會像這樣:- window => document =>
<html>
=><body>
=><table>
=><tbody>
=><tr>
- window => document =>
目標本身:
- 事件傳遞到目標本身
- 不會分捕獲或冒泡
- 以點選
<td>
標籤為例,事件傳遞就在目前這個目標<td>
本身
2.冒泡:
- 從子節點一路逆向傳回去根節點的過程
以點選
<td>
標籤為例,事件得傳遞可能會像這樣:<tr>
=><tbody>
=><table>
=><body>
=><html>
=> document => window
注意:掌握原則
1.先捕獲,再冒泡
2.當事件傳到 target 本身,若要裝監聽器的話,監聽器沒有分捕獲(左邊)跟冒泡(右邊),監聽器會裝在target 本身
3.捕獲與冒泡是「無論如何」都會發生的,而且順序永遠不會改變的一個東西。當你點擊某個按鈕時,就會先從 window 一路把事件由左邊傳遞下去,再從按鈕一路把事件傳遞由右邊傳遞回來。所以,儘管你什麼 event listener 都沒有加,背後的捕獲與冒泡還是存在。
事件機制也是這樣的,捕獲跟冒泡這個流程永遠都在,但如果你沒有加監聽器,你是察覺不到的。所以 addEventListener 的第三個參數只是覺得你要在「哪邊」加上這個監聽器,而不是改變原本事件傳遞的流程。
設定於哪個階段做監聽
- 在 .addEventListener() 加上第三個參數
- true:捕獲
- false:冒泡
- 若不寫第三個參數,則預設為 false
取消事件傳遞
e.stopPropagation()
- 加在哪個階段上,事件的傳遞就斷在哪裡,不會再把事件傳遞給「下一個節點」
.outer => .inner => .btn
加在.outer「捕獲階段」上
.outer「觸發」捕獲事件,不再傳遞給下一個節點
.outer捕獲 1加在.btn「冒泡階段」上
.btn「觸發」冒泡事件,不再傳遞給下一個節點
.outer捕獲1 => .inner捕獲1 => .btn冒泡2 => .btn捕獲2
停止後續 v.s 停止預設
e.stopPropagation
停止的是後續事件
e.stopImmediatePropagation()
讓其他同一層級的 listener 也不要被執行
e.preventDefault()
可以停止瀏覽器的預設行為
如果我想要阻止頁面上所有的 click
事件( 包括 <a>
預設的動作 ),可以在 window 物件監聽捕獲階段,來阻止底下的所有元素
window.addEventListener('click', function (e) {
e.preventDefault(); // 停止預設功能
e.stopPropagation(); // 停止後續傳遞
}, true) // 指定為從捕獲階段(左邊)開始監聽
// 底下的事件傳遞全都被阻止了
function addEvent(className) {
document.querySelector(className)
.addEventListener('click', function (e) {
console.log(className, '捕獲', e.eventPhase);
}, true)
document.querySelector(className)
.addEventListener('click', function (e) {
console.log(className, '冒泡', e.eventPhase);
}, false)
};
新手易混肴問題
利用迴圈幫全部按鈕加上事件監聽,希望利用迴圈的 i 值,在按鈕被點選的時候加上編號 i。
const btnGroup = document.querySelectorAll('.btn');
for (var i = 1; i <= btnGroup.length; i += 1) {
btnGroup[i].addEventListener('click', function (e) {
console.log(i);
})
}
但是實際上當 click 事件觸發時,迴圈已經跑完了(i 已經跑到 btnGroup.length),所以編號會通通是最後一號。
- 解決方式:
利用屬性 attribute 來紀錄每一圈新增按鈕時候的 i 值。這樣迴圈跑完後雖然 i 值已經跑到 btnGroup.length了, 每個按鈕依然有自己的 attribute 可以抓
<button class="btn" data-value="1">1</button>
<button class="btn" data-value="2">2</button>
<script>
const btnGroup = document.querySelectorAll('.btn');
for (let i = 0; i < btnGroup.length; i += 1) {
btnGroup[i].addEventListener('click', function (e) {
console.log(e.target.getAttribute('data-value'));
})
}
</script>
事件代理 Event delegation
利用事件傳遞的機制,把 button 的 eventListener 綁定在上層的 .outer 元素上,就叫做事件代理
<div class="outer">
<button class="btn_add" >add</button>
<button class="btn" data-value="1">1</button>
<button class="btn" data-value="2">2</button>
</div>
<script>
document.querySelector('.outer').addEventListener('click', function (e) {
console.log(e.target.getAttribute('data-value'));
})
</script>
什麼是 event delegation,為什麼我們需要它?
如果我們要把非常大量的「按鈕」加上監聽器,我們可以手動一個一個加,或者利用迴圈幫忙加上去。
但是我們有更方便的作法,就是把監聽器加在這些按鈕共同的「父層元素」。因為事件的傳遞從根節點到 target 的過程一定會經過這個「父層元素」。(上面有提到捕獲與冒泡是「無論如何」都會發生的,且事件的傳遞順序永遠不會改變。)這種由父層元素協助監聽的方式,就叫做「事件代理」。
- 比起手動一個一個加上監聽器,事件代理的效率更高
- 在這個父層元素底下動態新增的子元素,也可以一併綁定到 eventListener