請說明瀏覽器中的事件委派、捕獲、冒泡

2023年1月20日

💎 加入 E+ 成長計畫 與超過 500+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

在這篇文章你會知道

  • 事件委派、捕獲、冒泡是什麼?
  • event.targetevent.currentTarget 的差別
  • event.stopPropagation() 的用法

當使用者與瀏覽器互動時,會觸發各類不同的事件 (event),例如常見的點擊 (click)、滑動 (scroll)。我們可以透過 JavaScript 的事件處理器 (handler),來處理這些事件。讓我們能在事件觸發時,做出我們要的效果,例如點擊某個按鈕,觸發某個邏輯。

針對瀏覽器事件,最常見的考題之一,便是事件委派、事件捕獲、事件冒泡,是很常見的面試考題。以下將用第一人稱的擬答,來回答「請說明瀏覽器中的事件委派、捕獲、冒泡」這個問題。

事件委派

事件委派是當我們想要在一群子元素中,都加上同樣的事件監聽器與處理器時可以派上用場。當我們有許多相同元素,有相似的行為時,我們可以不用在每個元件都加上處理器,而是可以直接在父層加上處理器。這時透過 event.target 來得知實際上是哪一個元素發生事件,並處理該事件。

這種把監聽器與處理器裝在父層,然後委派給子元素,就是所謂的事件委派。這麼做的好處是,我們不用在每個元件,例如每個按鈕上都加上處理器,這可以減少記憶體消耗;這也讓我們的架構更彈性,可以隨時新增或移除元素。也可以寫比較少的程式碼,讓可閱讀性提升。

舉例來說 (編按:此例子來自 MDN),如果想要在一長串列表中的每個項目,都加上處理器,我們可以直接加在父層,不用每個子元素都加上,就算今天有上百上千個子元件都是。

<div id="container">
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
</div>;

const container = document.querySelector("#container");
container.addEventListener(
  "click",
  (event) => (event.target.style.backgroundColor = bgChange())
);

事件捕獲

事件委派之所以能夠發生,是因為在背後的事件捕獲與冒泡機制。一般來說,當事件觸發時,會先進入捕獲階段,然後到達事件目標,接著才是冒泡階段。(建議在面試時,可以簡單手繪這張事件流程,會更加幫助說明唷!)

image
圖片來源:https://javascript.info/bubbling-and-capturing

從上圖可得知,所謂的捕獲階段是指,當某個事件觸發時,例如使用者點了某個按鈕,此時由 DOM 樹的最上層 Window 一路往下,將事件傳遞下去並執行。實際在程式碼上,需要在事件監聽器中,加入 {capture: true} 來開啟捕獲機制。

事件冒泡

冒泡階段則是比較常用的,跟捕獲階段相反,它是先在目標上執行事件處理器,接著傳遞到父層,再傳到祖父層,然後一路傳上去。

<form onclick="alert('form 點擊事件觸發')">
  這是一個 form 元素
  <div onclick="alert('div 點擊事件觸發')">
    這是一個 div 元素
    <p onclick="alert('p 點擊事件觸發')">這是一個 p 元素</p>
  </div>
</form>

以上面的例子來說 (建議在面試時也可以簡單快速手寫這個例子,可以幫助說明),當我們在子層 <p> 裝一個 onclick 的處理器,點下去時,不僅該元素有跑出 alert ,其父層<div>onclick 也被觸發,然後祖父層 <form>onclick 也接續被觸發。

event.targetevent.currentTarget 的差別

前面提到事件委派時,我們提到了event.target這個屬性。而與這個屬性相似的是 event.currentTarget。,但它們之間存在一些重要的差異。

讓我們更深入地探討 event.targetevent.currentTarget 之間的差異:

  • event.target
    • 指向觸發事件的元素
    • 在傳播過程中,event.target 的值保持不變
    • 例如,如果在一個按鈕上添加點擊事件監聽器,當按鈕被點擊時,event.target 將始終指向該按鈕元素
  • event.currentTarget
    • 指向當前正在處理事件的元素,即事件監聽器所附加的元素
    • 在事件傳播過程中,event.currentTarget 的值可能會發生變化
    • 例如,如果在一個父元素上添加點擊事件監聽器,當其子元素被點擊時,事件會冒泡到父元素,此時 event.currentTarget 將指向父元素

根據具體的需求,我們可以選擇使用 event.targetevent.currentTarget 來實現不同的功能。如果我們想要知道事件的真正來源,並對其進行操作,可以使用 event.target。如果我們想要在事件冒泡或捕獲過程中對當前處理事件的元素進行操作,可以使用 event.currentTarget

阻止冒泡:event.stopPropagation()

在實務上,我們有時候不想要冒泡,例如只想要子元素的事件被觸發,不想要父層的元素被觸發,避免干擾。這時候想要不發生冒泡,可以在處理器加上 event.stopPropagation(),不過這個仍會讓該處理器執行,只是不會冒泡上去;如果連該處理器的其他事件類別都不想執行的話,可以用 event.stopImmediatePropagation()

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們