本篇內容參考連結
TechBridge 技術共筆部落格
假如網頁做久了, 一定會用到下面幾個關鍵字的函式 addEventListener
, preventDefault
和 stopPropagation
. 而這兩個(缺少哪一個之後會揭曉)使用的方式將會跟今天的主題有很大的關係.簡單來說, 就是要講述事件觸發後, 在各個dom物件傳遞的順序.
這個機制主要分為三個部分, 依照順序分別為
可以先參考以下的圖來理解
首先要知道整個HTML的架構會像是一個樹狀圖, 然後當你點擊了<table>
裡面的其中一個<td>
, 事件並不是從<td>
開始觸發和傳遞, 而是從最上層Window開始一步一步傳下來直到目標, 而這個階段就是CAPTURING_PHASE. 當事件到達目標<td>
後, 會進入下一個階段AT_TARGET. 然後再慢慢地往回飄上去(因為是Bubble), 這個階段就是BUBBLING_PHASE. (口訣 : 先捕獲, 在冒泡)
而JS裡面聆聽事件, 並做callback的函式, 就是addEventListener
. 其實他還有第三個參數, true
代表添加這個callback到捕獲階段, 而false
則是加到冒泡階段.
借用上面這一張圖, 寫了一個測試範例
eventPhase : 事件的階段
1: Capturing
2: At Target
3: Bubbleing
<!DOCTYPE html>
<html>
<body>
<ul id="ul_item">
<li id="li_item">
<a id="a_link">trigger Here</a>
</li>
</ul>
<script>
const ul_item = document.getElementById("ul_item");
const li_item = document.getElementById("li_item");
const a_link = document.getElementById("a_link");
// ul 的捕獲
ul_item.addEventListener('click', (e) => {
console.log('ul capturing', e.eventPhase);
}, true)
// ul 的冒泡
ul_item.addEventListener('click', (e) => {
console.log('ul bubbling', e.eventPhase);
}, false)
// li 的捕獲
li_item.addEventListener('click', (e) => {
console.log('li capturing', e.eventPhase);
}, true)
// li 的冒泡
li_item.addEventListener('click', (e) => {
console.log('li bubbling', e.eventPhase);
}, false)
// a 的捕獲
a_link.addEventListener('click', (e) => {
console.log('a capturing', e.eventPhase);
}, true)
// a 的冒泡
a_link.addEventListener('click', (e) => {
console.log('a bubbling', e.eventPhase);
}, false)
</script>
</body>
</html>
當點擊#a_link時, 結果得到 :
從範例中就可以看到實際運行狀況, 不管順序如何改變, 都會符合先捕捉後冒泡的順序.
再參考文件中有提到另一個測試, 當在Target Element, 把Capture listener和Bubble listener的編寫順序顛倒, 會得到先輸出 Bubble listener, 這是因為都在Target Phase, 所有就沒有捕捉或冒泡的關係, 從而根據加入Macrotask的順序來做輸出. 然後也是根據不同的瀏覽器會有不一樣的輸出.
在新版的 Chrome 似乎更改了這個行為,請參考:Chrome 89 更新事件触发顺序,导致99%的文章都错了(包括MDN)
在Target Phase, 也會依照先捕捉後冒泡的順序來輸出.
stopPropagation()
從字面上的意思就可以知道事件到了這個listener就會停止傳遞到下一個節點, 但在同一個element不同listener還是會被觸發. 若想讓同一層級的listner不被執行,則可改用stopImmediatePropagation()
;
這個函式則僅是取消Dom的預設行為 (例如: 像是我們點擊超連結<a>
,他的預設行為是開新分頁或跳轉, 當listener添加了 preventDefault()
後, 則就會取消此行為), 並不會影響事件的傳遞, 但會影響之後節點的預設行為, 並一併取消(例如: 當在母節點中, 添加preventDefault()
後, 等事件傳遞到之後的超連結, 也都會失效).所以上面三個函式中, 就是他與事件傳遞機制無關.