简述 JavaScript 的事件捕获和事件冒泡

9/8/2021 JavaScript

JavaScript 事件冒泡是为了捕捉和处理 DOM 内部传播的事件。但是你知道事件冒泡和事件捕获之间的区别吗?

在这篇文章中,我将用相关的示例来讨论关于这个主题你所需要了解的全部情况。

# 事件流的传播

在介绍事件捕获和事件冒泡之前,先来看下一个事件是如何在 DOM 内部传播的。

如果我们有几个嵌套的元素处理同一个事件,我们会对哪个事件处理程序会先触发的问题感到困惑。这时,理解事件传播顺序就变得很有必要。

通常,一个事件会从父元素开始向目标元素传播,然后它将被传播回父元素。

JavaScript 事件分为三个阶段:

  • 捕获阶段:事件从父元素开始向目标元素传播,从 Window 对象开始传播。
  • 目标阶段:该事件到达目标元素或开始该事件的元素。
  • 冒泡阶段:这时与捕获阶段相反,事件向父元素传播,直到 Window 对象。

下图将让你进一步了解事件传播的生命周期:

DOM 事件流

现在你大概了解了 DOM 内部的事件流程,让我们再来看下事件捕获和冒泡是如何出现的。

# 什么是事件捕获

事件捕获是事件传播的初始场景,从包装元素开始,一直到启动事件生命周期的目标元素。

如果你有一个与浏览器的 Window 对象绑定的事件,它将是第一个被执行的。所以,在下面的例子中,事件处理的顺序将是 WindowDocumentDIV 2DIV 1,最后是 button

事件捕获示例

这里我们可以看到,事件捕获只发生在被点击的元素或目标上,该事件不会传播到子元素。

我们可以使用 addEventListener() 方法的 useCapture 参数来注册捕捉阶段的事件。

target.addEventListener(type, listener, useCapture);

你可以使用下面的代码来测试上述示例,并获得事件捕获的实践经验。

window.addEventListener("click", () => {
    console.log('Window');
  },true);

document.addEventListener("click", () => {
    console.log('Document');
  },true);

document.querySelector(".div2").addEventListener("click", () => {
    console.log('DIV 2');
  },true);

document.querySelector(".div1").addEventListener("click", () => {
    console.log('DIV 1');
  },true);

document.querySelector("button").addEventListener("click", () => {
    console.log('CLICK ME!');
  },true);

# 什么是事件冒泡

如果你知道事件捕获,事件冒泡就很容易理解,它与事件捕获是完全相反的。

事件冒泡将从一个子元素开始,在 DOM 树上传播,直到最上面的父元素事件被处理。

addEventListener() 中省略或将 useCapture 参数设置为 false,将注册冒泡阶段的事件。所以,事件监听器默认监听冒泡事件。

事件冒泡示例

在我们的示例中,我们对所有的事件使用了事件捕获或事件冒泡。但是如果我们想在两个阶段内都处理事件呢?

让我们举个例子,在冒泡阶段处理 DocumentDIV 2 的点击事件,其他事件则在捕获阶段处理。

注册两个阶段的事件

连接到 WindowDIV 1button 的点击事件将在捕获过程中分别触发,而 DIV 2Document 监听器则在冒泡阶段依次触发。

window.addEventListener("click", () => {
    console.log('Window');
  },true);

document.addEventListener("click", () => {
    console.log('Document');
  }); // 已注册为冒泡

document.querySelector(".div2").addEventListener("click", () => {
    console.log('DIV 2');
  }); // 已注册为冒泡

document.querySelector(".div1").addEventListener("click", () => {
    console.log('DIV 1');
  },true);

document.querySelector("button").addEventListener("click", () => {
    console.log('CLICK ME!');
  },true);

我想现在你已经对事件流、事件冒泡和事件捕获有了很好的理解。那么,让我们看下什么时候可以使用事件冒泡和事件捕获。

# 事件捕获和冒泡的应用

通常情况下,我们只需要在全局范围内执行一个函数,就可以使用事件传播。例如,我们可以注册文档范围内的监听器,如果 DOM 内有事件发生,它就会运行。

同样地,我们可以使用事件捕获和冒泡来改变用户界面。

假设我们有一个允许用户选择单元格的表格,我们需要向用户显示所选单元格。

演示

在这种情况下,为每个单元格分配事件处理程序将不是一个好的做法。它最终会导致代码的重复。

作为一个解决方案,我们可以使用一个单独的事件监听器,并利用事件冒泡和捕获来处理这些事件。

因此,我为 table 创建了一个单独的事件监听器,它将被用来改变单元格的样式。

document.querySelector("table").addEventListener("click", (event) => {
  if (event.target.nodeName == "TD")
    event.target.style.background = "rgb(230, 226, 40)";
});

在事件监听器中,我使用 nodeName 来匹配被点击的单元格,如果匹配,单元格的颜色就会改变。

# 如何防止事件传播

有时,如果事件冒泡和捕捉开始不受我们控制地传播时,就会让人感到厌烦。

如果你有一个严重嵌套的元素结构,这也会导致性能问题,因为每个事件都会创建一个新的事件周期。

当冒泡变得烦人时

在上述情况下,当我点击删除按钮时,包装元素的点击事件也被触发了。这是由于事件冒泡导致的。

我们可以使用 stopPropagation() 方法来避免这种行为,它将阻止事件沿着 DOM 树向上或向下进一步传播。

document.querySelector(".card").addEventListener("click", () => {
  $("#detailsModal").modal();
});

document.querySelector("button").addEventListener("click", (event) => {
  event.stopPropagation(); // 停止冒泡
  $("#deleteModal").modal();
});

使用  后

# 本文总结

JavaScript 事件捕获和冒泡可以用来有效地处理 Web 应用程序中的事件。了解事件流以及捕获和冒泡是如何工作的,将有助于你通过正确的事件处理来优化你的应用程序。

例如,如果你的应用程序中有任何意外的事件启动,了解事件捕获和冒泡可以节省你排查问题的时间。

因此,我希望你尝试上述示例并在评论区分享你的经验。

感谢阅读!