JavaScript處理DOM事件上的獲取和冒泡,實務上滿常用到的觀念,可以透過這方式解決一些麻煩問題,例如: popup視窗的關閉、內外層DOM互動關係,另外事件獲取、冒泡也幾乎是面試必考題。

JavaScript logo

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為止。

EventListener usecapture MDN

JavaScript dompass

// 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。