深入理解 JavaScript 事件冒泡机制
在探索 Web 开发的奥秘时,我发现事件冒泡是一种既令人惊讶又极具启发性的行为。 刚开始接触它时,可能会觉得有些反常,但深入理解其工作原理后,你会发现它其实非常符合逻辑。 作为 Web 开发人员,你不可避免地会遇到事件冒泡。 那么,究竟什么是事件冒泡呢?
为了实现用户与网页的互动,JavaScript 依赖于事件机制。 事件本质上是指代码可以检测并响应的动作或操作。 例如,鼠标点击、键盘按键和表单提交等都属于事件的范畴。
JavaScript 使用事件监听器来监测和响应事件。 事件监听器本质上是一个函数,它会监听或等待页面上特定事件的发生。 比如,一个按钮的点击事件。 当事件监听器检测到其正在监听的事件时,它会执行与该事件关联的代码,从而做出响应。 捕捉和处理事件的整个流程称为事件处理。
现在,我们假设页面上有三个元素:一个 div、一个 span 和一个 button。 Button 元素嵌套在 span 元素中,而 span 元素又嵌套在 div 元素中。 下图展示了它们之间的层级关系:
假设这三个元素都分别绑定了事件监听器,用于监听点击事件并在被点击时在控制台打印信息,那么当你点击按钮时会发生什么呢?
为了亲自验证,你可以创建一个文件夹,并在其中创建三个文件:一个名为 index.html 的 HTML 文件,一个名为 style.css 的 CSS 文件,以及一个名为 app.js 的 JavaScript 文件。
在 HTML 文件中,添加以下代码:
<html lang="en"> <head> <title>Event bubbling</title> <link rel="stylesheet" href="https://wilku.top/the-hidden-key-to-dynamic-web-interactions/style.css"> </head> <body> <div> <span><button>Click Me!</button></span> </div> <script src="app.js"></script> </body> </html>
在 CSS 文件中,添加以下代码,用于设置 div 和 span 元素的样式。
div { border: 2px solid black; background-color: orange; padding: 30px; width: 400px; } span { display: inline-block; background-color: cyan; height: 100px; width: 200px; margin: 10px; padding: 20px; border: 2px solid black; }
在 JavaScript 文件中,添加以下代码,为 div、span 和 button 元素分别添加事件监听器。 这些事件监听器都监听点击事件。
const div = document.querySelector('div'); div.addEventListener('click', () => { console.log("You've clicked a div element") }) const span = document.querySelector('span'); span.addEventListener('click', () => { console.log("You've clicked a span element") }) const button = document.querySelector('button'); button.addEventListener('click', () => { console.log("You've clicked a button") })
现在在浏览器中打开 HTML 文件。 检查页面,然后点击页面上的按钮。 你发现了什么? 点击按钮的输出结果如下:
点击按钮不仅触发了按钮上的事件监听器,span 和 div 元素上的事件监听器也被触发了。 你可能会感到疑惑,为什么会出现这种情况呢?
点击按钮会触发按钮自身绑定的事件监听器,从而在控制台打印信息。 但是,由于按钮嵌套在 span 元素中,点击按钮的行为在技术上也等同于点击了 span 元素,因此也会触发 span 的事件监听器。
同理,由于 span 元素又嵌套在 div 元素中,点击 span 元素也意味着点击了 div 元素,因此 div 的事件监听器也会被触发。 这就是事件冒泡的原理。
事件冒泡
事件冒泡是指,在嵌套的 HTML 元素集合中,当某个元素上触发事件时,这个事件会从触发它的最内层元素开始,向上“冒泡”传递,直到 DOM 树的根元素,从而触发所有监听该事件的事件监听器。
事件监听器会按照特定的顺序依次触发,这个顺序与事件在 DOM 树中冒泡或传播的方式一致。 考虑下图所示的 DOM 树结构,它反映了本文中使用的 HTML 结构:
事件在 DOM 树中冒泡的示意图
DOM 树展示了 button 嵌套在 span 中,span 嵌套在 div 中,div 嵌套在 body 中,而 body 又嵌套在 HTML 元素中。 当你点击 button 时,由于元素之间是嵌套的关系,点击事件首先会触发 button 上绑定的事件监听器。
接着,由于元素的嵌套关系,事件会沿着 DOM 树向上“冒泡”,传递到 span 元素,然后是 div 元素,接着是 body 元素,最后传递到 HTML 元素,从而触发监听该点击事件的所有事件监听器。这些监听器会按照它们在 DOM 树中的层级关系依次执行。
这就是为什么 span 和 div 元素的事件监听器也被执行的原因。 如果 body 和 HTML 元素上也绑定了点击事件监听器,它们同样会被触发。
事件发生的 DOM 节点称为目标节点。 在我们的例子中,因为点击事件发生在 button 上,所以 button 元素是事件的目标节点。
如何阻止事件冒泡
为了阻止事件在 DOM 中“冒泡”,我们可以使用事件对象提供的一个名为 stopPropagation()
的方法。 考虑以下代码示例,我们用它来为 button 元素添加事件监听器:
const button = document.querySelector('button'); button.addEventListener('click', () => { console.log("You've clicked a button"); })
当用户点击按钮时,这段代码会导致事件在 DOM 树中“冒泡”。 为了阻止事件冒泡,我们需要调用 stopPropagation()
方法,如下所示:
const button = document.querySelector('button'); button.addEventListener('click', (e) => { console.log("You've clicked a button"); e.stopPropagation(); })
事件处理程序是在点击按钮时执行的函数。 事件监听器会自动将事件对象传递给事件处理程序。 在我们的例子中,这个事件对象由变量名 `e` 表示,它作为参数传递给事件处理程序。
事件对象包含有关事件的信息,并且允许我们访问与事件相关的各种属性和方法。 其中一种方法就是 stopPropagation()
,它被用于阻止事件的冒泡。 在 button 的事件监听器中调用 stopPropagation()
可以阻止事件从 button 元素向上“冒泡”到 DOM 树的更上层元素。
添加 stopPropagation()
方法后,点击按钮的结果如下:
我们使用 stopPropagation()
来阻止事件从我们使用该方法的元素开始向上冒泡。 比如,如果我们想让点击事件从 button 元素向上冒泡到 span 元素,而不是继续在 DOM 树中向上冒泡,我们可以在 span 的事件监听器中使用 stopPropagation()
方法。
事件捕获
事件捕获与事件冒泡恰好相反。 在事件捕获中,事件从最外层元素向下传递到目标元素,就像这样:
事件捕获示意图
例如,在我们的例子中,当点击 button 元素时,在事件捕获中,div 元素上的事件监听器会首先被触发,然后是 span 元素上的监听器,最后才是目标元素(即 button 元素)上的监听器。
然而,事件冒泡是事件在文档对象模型 (DOM) 中传播的默认方式。 要将默认行为从事件冒泡更改为事件捕获,我们需要给事件监听器传递第三个参数,将事件捕获设置为 true
。 如果你没有给事件监听器传递第三个参数,事件捕获默认设置为 false
。
考虑以下事件监听器:
div.addEventListener('click', () => { console.log("You've clicked a div element") })
由于没有第三个参数,捕获默认设置为 false
。 要将捕获设置为 true
,我们需要传递第三个参数,即布尔值 true
,它会将捕获设置为 true
。
div.addEventListener('click', () => { console.log("You've clicked a div element") }, true)
或者,你也可以传入一个对象,将 capture
属性设置为 true
,如下所示:
div.addEventListener('click', () => { console.log("You've clicked a div element") }, {capture: true})
要测试事件捕获,你可以在 JavaScript 文件中,将第三个参数添加到所有的事件监听器,如下所示:
const div = document.querySelector('div'); div.addEventListener('click', () => { console.log("You've clicked a div element") }, true) const span = document.querySelector('span'); span.addEventListener('click', (e) => { console.log("You've clicked a span element") }, true) const button = document.querySelector('button'); button.addEventListener('click', () => { console.log("You've clicked a button"); }, true)
现在打开浏览器,点击 button 元素。 你应该会得到如下输出:
请注意,与事件冒泡中首先打印 button 输出不同,在事件捕获中,第一个输出来自最外层元素 div。
事件冒泡和事件捕获是事件在 DOM 中传播的两种主要方式。 但是,事件冒泡通常是事件传递的首选方式。
事件委托
事件委托是一种设计模式,它将单个事件监听器附加到公共父元素(比如 <ul>
元素),而不是在每个子元素上都绑定事件监听器。 然后,子元素上的事件向上“冒泡”到父元素,并由父元素上的事件处理程序进行处理。
因此,如果有一个父元素包含多个子元素,我们只需要给父元素添加一个事件监听器,这个事件处理程序将处理所有子元素上的事件。
你可能会想,父元素是如何知道哪个子元素被点击的呢? 如前所述,事件监听器会将事件对象传递给事件处理程序。 这个事件对象包含各种方法和属性,用于提供关于特定事件的信息。 事件对象中的一个重要属性是 target
属性。 target
属性指向实际发生事件的特定 HTML 元素。
例如,如果我们有一个无序列表,其中包含多个列表项,并且我们将事件监听器附加到 <ul>
元素上,当任何一个列表项上发生事件时,事件对象中的 target
属性就会指向事件发生的特定列表项。
为了更好地理解事件委托的实际应用,请将以下 HTML 代码添加到你现有的 HTML 文件中:
<ul> <li>Toyota</li> <li>Subaru</li> <li>Honda</li> <li>Hyundai</li> <li>Chevrolet</li> <li>Kia</li> </ul>
添加以下 JavaScript 代码,使用事件委托,通过父元素上的单个事件监听器来监听子元素上的事件:
const ul = document.querySelector('ul'); ul.addEventListener('click', (e) => { // target element targetElement = e.target // log out the content of the target element console.log(targetElement.textContent) })
现在打开浏览器,点击列表中的任意项目。 该项目的内容应该会被打印到控制台,就像这样:
通过使用单个事件监听器,我们就可以处理所有子元素上的事件。 如果页面上有大量的事件监听器,会对页面的性能产生影响,消耗更多的内存,并导致页面加载和渲染缓慢。
事件委托允许我们通过减少页面上需要使用的事件监听器的数量来避免这些问题。 事件委托依赖于事件冒泡。 因此,我们可以说事件冒泡有助于优化网页的性能。
高效事件处理的技巧
作为开发人员,在处理文档对象模型中的事件时,建议你考虑使用事件委托,而不是在页面上的许多元素上都绑定事件监听器。
当使用事件委托时,请记住将事件监听器附加到需要事件处理的子元素的最近公共祖先。 这有助于优化事件冒泡,并最大限度地减少事件在处理之前必须经过的路径。
在处理事件时,善用事件监听器提供的事件对象。 事件对象包含诸如 target
之类的属性,它们在事件处理过程中非常有用。
为了获得性能更高的网站,请尽量避免过度的 DOM 操作。 频繁触发 DOM 操作的事件可能会对网站的性能产生负面影响。
最后,对于嵌套元素,在为元素添加嵌套的事件监听器时要格外小心。 这不仅会影响性能,还会使事件处理变得异常复杂,导致代码难以维护。
总结
事件是 JavaScript 中一项强大的工具。 事件冒泡、事件捕获和事件委托是 JavaScript 中处理事件的关键概念。 作为 Web 开发人员,请利用本文的内容来熟悉这些概念,以便构建出更具交互性、动态性和高性能的网站和应用程序。