请说明浏览器中的事件委派、捕获、冒泡

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 上追蹤我們