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

2023年1月20日

💎 加入 E+ 成長計畫 與超過 250+ 位軟體工程師一起在社群中成長,並且獲得更深入、系統性軟體工程內容

當使用者與瀏覽器互動時,會觸發各類不同的事件 (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())
);

事件捕獲

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

image
圖片來源:W3C

從上圖可得知,所謂的捕獲階段是指,當某個事件觸發時,例如使用者點了某個按鈕,此時由 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 也接續被觸發。

這邊有個細節需要分別,在冒泡時的 this 不必然等於 event.target,而是會等於 event.currentTarget 。換句話說,this 是正在執行的處理器 (會一直變成下一個);而 event.target 一直都會是真正變點擊的那個 (在這邊就是最裡頭的子層)。

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

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