JavaScript 處理 DOM 事件上的獲取和冒泡,實務上滿常用到的觀念,可以透過這方式解決一些麻煩問題,例如: popup 視窗的關閉、內外層 DOM 互動關係,另外事件獲取、冒泡也幾乎是面試必考題。
DOM 一般事件綁定
下面的例子,我 container 先綁定 click 事件,再綁定 first,各自彈出自己的 id 名稱,那哪個會先 alert 出來。
<div id="container">container
<ul id="list">ist
<li id="first">number 1</li>
</ul>
</div>
...
<script>
document.getElementById('container').addEventListener('click',function(e){
alert(this.id);
});
document.getElementById('first').addEventListener('click',function(e){
alert(this.id);
});
</script>
會是 first 先彈跳出來,因為綁定事件順序並不是代表執行順序,單純只是哪個 DOM 先綁定事件監聽,實際執行序還是要依照 DOM 父子關係判定,除非是綁定到同個 DOM 上,才會依照先後綁定順序執行。
事件傳遞 Capture Bubble
我們可以在 addEventListener(‘click’,function(){}, true
),來決定 useCapture 參數的 boolean,預設沒帶會是設為 false,當 usecaptue 為 true 時,事件觸發會先經由 DOM tree 一路往子層到目標為止,之後再冒泡上去父層,這樣一個完整的流程就是事件獲取與冒泡。
最重要就是我們有辦法阻止事件獲取冒泡的傳遞,利用 event.stopPropagation function,就可以阻止事件往後傳遞。
另外 event 還有提供物件 eventPhase,會回傳 0~4 的數值,讓我們可以清楚知道這個事件到什麼階段。
eventPhase: 0 沒有事件
eventPhase: 1 獲取階段,會以物件父層一直到最高開始執行,最頂端會是Window,
再來Document,Html,一直到目標為止。
eventPhase: 2 目標階段,這代表事件執行到目標
eventPhase: 3 冒泡階段,會由目標物件的第一層父層開始,一路往上到最頂端window為止。
// get All elements
const nodeList = [...document.querySelectorAll("*")];
// add All elements eventListener
nodeList.forEach((elem) => {
// use true is for capture
elem.addEventListener(
"click",
(e) => alert(`capture ${elem.tagName} phase:${e.eventPhase}`),
true
);
// default false is for bubble
elem.addEventListener("click", (e) =>
alert(`bubble: ${elem.tagName} phase:${e.eventPhase}`)
);
});
MDN eventphase flow (MDN 教學)
Capture Bubble 實用範例
這邊我們用最經典的 popup 例子,需求是要點擊按鈕 click 會讓 popup 顯示的,但在這邊我們希望 popup 內部點擊不會關閉 popup,但是點外部隨便空間會關閉 popup。
首先監聽 body 點擊會關閉 popup,再來監聽 openPop 按鈕點擊讓 popup 打開,這邊還多下了 e.stopPropagation()
;,防止我點擊 popup 打開觸發 body 點擊被關閉,因為這樣可以中斷點擊 popup 往上冒泡觸發 body 事件。
再來是 popup 本身需要能點選內部內容,同樣我們也對 popup 使用 e.stopPropagation(); ,讓我們可以點擊 popup 裡面的內容、按鈕。
<button id="openPop">open popup</button>
<div id="text"></div> // fake wording
<div id="popup" class="">
<span id="closePop">x</span>
<div>
This is popup
<button onclick="alert('hello')">Alert Button</button>
</div>
</div>
...
const popup = document.getElementById('popup');
document.getElementById('openPop').addEventListener('click',(e)=>{
e.stopPropagation();
popup.classList.add("active");
});
document.body.addEventListener('click',()=>{
close();
});
popup.addEventListener('click',(e)=>{
e.stopPropagation();
});
document.getElementById('closePop').addEventListener('click',()=>{
close();
});
function close () {
popup.classList.remove('active');
}
心得
這觀念真心覺得實用,很多疑難雜症可以處理,或是可以少綁一些 eventlistener,利用父層子層獲取冒泡關係,搭配 stopPropagation。