2023年8月26日 星期六

[Javascript] stopPropagation 停止事件捕捉及冒泡

近期工作上修正一個因為冒泡(bubbling) 導致的錯誤,神奇的是 chrome 異常但 safari 正常,因此想知道兩個瀏覽器為什麼會有這樣的差異,順便整理一下stopPropagation 的使用方法。

冒泡(bubbling) 是 Javacript 事件傳遞的方式,當一個 div 物件裡有個按鈕(button),也就是 div 為 button 的父元件,兩者都監聽點擊(click) 事件,當使用者點擊按鈕時,按鈕會先被觸發並進一步傳遞給 div 物件觸發點擊事件。

因此依據這樣的特性,可以將事件監聽 (addEventListener) 放在共同的父元件上,達到效能的優化,動態產生 DOM 元件時也不必額外處理事件監聽

<!-- 比較兩者差異 -->
<ul>
    <li onclick="doSomething()"> ... </li>
    <li onclick="doSomething()"> ... </li>
</ul>

<ul onclick="doSomething()">
    <li> ... </li>
    <li> ... </li>
</ul>

但總會有一些例外,例如只希望觸發目標元件(event.target) 的事件,而 stopPropagation 就是可以阻止事件傳播冒泡。

<div id="c">
    <div id="b">
        <button id="a">click</button>
    </div>
</div>

對每一層物件都加上監聽

<script>
document.querySelector("#a").addEventListener('click', event => {
    console.log('a');
});
document.querySelector("#b").addEventListener('click', event => {
    console.log('b');
});
document.querySelector("#c").addEventListener('click', event => {
    console.log('c');
});
</script>

若點擊按鈕,會依需印出 a b c


接下來,其他設定不變,我們稍微調整 #b 加上 stopPropagation

document.querySelector("#b").addEventListener('click', event => {
    event.stopPropagation(); // HERE!!!!

    console.log('b');
});

若點擊按鈕,會依需印出 a b,最外層並不會執行。


另外有 stopImmediatePropagation 極為相似,但不只是停止冒泡到父元件,同一個元件的相同事件也會被停止。舉例 #b 不只是一項 click 事件監聽:

document.querySelector("#b").addEventListener('click', event => {
    event.stopImmediatePropagation(); // HERE!!

    console.log('b');
});
document.querySelector("#b").addEventListener('click', event => {
    console.log('other');
});

若點擊按鈕,也是印出 a b,包含最外層 cother都不會執行。


最後,safari 冒泡的行為我參考 Mouse event bubbling in iOS 這篇文章,必須符合一些情境才會冒泡,ios chrome 也會是相同的實作,但不確定為什麼 116.0.5845.96 這個版本的 chrome 突然又會冒泡。

這次的情境是子元件可以點擊編輯、父元件可以點擊拖曳,因為冒泡所以最後都會進入拖曳的狀態而無法編輯。之前因為大多使用 ios 瀏覽器操作系統,所以沒發現這個 bug,windows 系統下,若子元件未加上 stopPropagation 一樣會遇到無法編輯的問題。