JS 中的事件冒泡 & 捕獲是前端必須要了解的重要原理,不然會發生 怎麼點了一個元素其他事件跟著一起噴出來 的奇怪問題。
以下面的 HTML 為例:
<div class="outer">
<div class="inner"></div>
</div>
inner
被包在 outer
裡面,所以當我們點擊 inner
時,究竟是只點到 inner
,還是連 outer
一起點了?
實際上是兩個都有點到。
上圖可以看到,當我們點擊 <td>
時,事件會從 Window 一路往下傳、傳到 <td>
後再一路傳回去 Window,所以上面的例子才會說兩個都有點擊到。
上面提到的「從 Window 一路往下傳」,這個階段就叫做事件捕獲(Event Capturing)。相反的從 <td>
一路往上到 Window 的階段就是事件冒泡(Event Bubbling)。
所以常常會看到一個重要的口訣:
先捕獲,再冒泡。
看到這邊我們就知道一個事件會有三個階段:
我們常用的 addEventListener
能夠決定要在捕獲還是冒泡階段執行某個事件。決定的方法是在 addEventListener
的第三個選項(capture)加入布林。
outer.addEventListener('click', () => {
console.log("outer")
}, {capture: true})
// 或是寫成以下
outer.addEventListener('click', () => {
console.log("outer")
}, true)
使用 true
的話,表示 listener 是在捕獲階段執行、false
則相反。
如果不特別設定(空白)的話,預設就會是 false
。
有時候我們希望點擊的時候只觸發自己本身的事件,例如:
outer.addEventListener('click', (e) => {
console.log('outer')
})
inner.addEventListener('click', (e) => {
console.log('inner')
})
結果按下 inner
後,兩個 log 都出來了,原因是因為事件冒泡。要解決這個也很簡單,使用 event.stopPropagation()
就能阻止事件往下一個節點前進。
inner.addEventListener('click', (e) => {
console.log('inner')
event.stopPropagation()
})
大多數事件都會冒泡。
為什麼用「大多數」?因為像 focus
、blur
等事件是不會冒泡的。
我們在替 <ul>
內的 <li>
個別添加事件時,會這樣寫:
// HTML
<ul>
<li>首頁</li>
<li>商品頁</li>
<li>搜尋頁</li>
</ul>
// JS
const el = document.querySelectorAll("li");
// 針對各個 li 添加事件
el.forEach(item => {
item.addEventListener("click", (e) => {
console.log(e.target);
})
});
雖然可以成功的替每個元素增加事件,但如果我們在 <ul>
中新增了新的 <li>
,新增的元素就吃不到事件了。
所以在理解了事件機制後,我們可以將 JS 的程式碼改為:
const list = document.querySelector('ul');
list.addEventListener("click", (e) => {
// 判斷點擊到 li 標籤後觸發事件
if(e.target.tagName === "LI"){
// ...
}
});
除了更方便管理外、也解決了新增 DOM 抓不到事件的問題。