JavaScript事件机制详解:捕获、冒泡与事件委托
目录
1.什么是JavaScript的事件机制?
2.DOM树
3.事件触发流程
4. 如何利用事件冒泡机制(事件委托)
5.如何阻止事件冒泡
6.总结
1.什么是JavaScript的事件机制?
JavaScript的事件机制一共分为三个部分,分别是事件捕获,事件冒泡和事件委托。
(1)事件捕获:指的是事件从根节点(document节点)向目标事件传播的过程。
(2)事件冒泡:指的是当捕获到目标事件之后从目标事件开始向上传播(传播至Document节点)的过程。
(3)事件委托:指的是利用事件冒泡机制,在对应子元素的父元素节点上绑定事件处理器,避免对多个子元素分别绑定事件监听器。这样做可以显著减少内存占用和事件处理器的数量,提升页面性能,尤其是在子元素数量很多或动态生成的场景下,事件委托能降低浏览器的事件管理开销,提高响应效率和代码维护性。
想要掌握好JavaScript中的事件机制,首先掌握好事件是如何被捕获的(事件捕获阶段),捕获到了事件是怎么触发的(事件执行阶段),事件触发之后会怎么样(事件冒泡阶段)。理解这些之后就知道如何利用事件冒泡来进行事件委托了。
2.DOM树
在讲述事件机制之前,首先掌握一个概念,什么是DOM树。理解了DOM树可以让我们更好的理解事件的触发和冒泡过程。
什么是DOM树?
- DOM(文档对象模型,Document Object Model)是浏览器解析HTML后生成的一个树状结构。
- 每个HTML标签、文本节点、属性都会被表示成DOM树上的一个节点。
- 通过DOM树,JavaScript可以访问、修改网页内容和结构。
案例:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><div id="container"><button id="btn">点击我</button></div></body>
</html>
DOM树为:
Document
└── html
└── body
└── div#container
└── button#btn (文本节点: "点击我")
3.事件触发流程
假设我们在上面HTML中给按钮和它的父元素div都绑定点击事件:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><div id="container"><button id="btn">点击我</button></div><script>const container = document.getElementById('container');const btn = document.getElementById('btn');container.addEventListener('click', () => {console.log('容器被点击');});btn.addEventListener('click', () => {console.log('按钮被点击');});</script></body>
</html>
事件触发流程:
- 当用户点击按钮时,浏览器根据DOM树找到事件目标(button)。
- 事件先从document开始捕获,经过html、body、div,最后到button(捕获阶段)。明事件捕获默认是不启用的,需要在
addEventListener第三个参数设置为true才会执行捕获阶段的监听。否则事件监听默认只在目标阶段和冒泡阶段触发。 - 事件在button上触发(目标阶段)。
- 事件从button向上冒泡,经过div、body、html、document(冒泡阶段)。
- 绑定在button和container上的事件都会被触发,输出:按钮被点击 容器被点击。
从上述案例我们可以看出,当我们在给一个事件绑定了监听器之后,如果该监听器被触发了,浏览器首先需要找到目标元素(捕获阶段),找到目标元素之后浏览器会触发当前元素绑定的事件监听器的事件(目标阶段),当执行完当前的事件之后会继续沿着DOM树向上触发(冒泡阶段),由于<div id="container">是该按钮的父元素并且也绑定了事件监听,所以也会触发,直至传递到根节点。
4. 如何利用事件冒泡机制(事件委托)
假设你有一个列表,里面有很多条目,每条目都有一个“删除”按钮:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><ul id="list"><li>项目1 <button class="delete-btn">删除</button></li><li>项目2 <button class="delete-btn">删除</button></li><li>项目3 <button class="delete-btn">删除</button></li><!-- 可能还有更多项目 --></ul></body>
</html>
(1)传统做法(不使用事件委托):
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><ul id="list"><li>项目1 <button class="delete-btn">删除</button></li><li>项目2 <button class="delete-btn">删除</button></li><li>项目3 <button class="delete-btn">删除</button></li><!-- 可能还有更多项目 --></ul><script>const deleteButtons = document.querySelectorAll('.delete-btn');deleteButtons.forEach(btn => {btn.addEventListener('click', function() {const li = this.parentElement;li.remove();console.log('删除了一个项目');});});</script></body>
</html>
我们发现,如果想要点击按钮就删除对应元素,我们可以给每一个按钮都绑定事件监听器来达到效果。这样对于少部分的事件其实并没有什么影响,但是如果事件很多的情况下,绑定大量事件监听器会消耗内存和性能。而且如果后来还需要动态添加新的项目,则需要额外绑定事件。
(2)利用事件冒泡机制(事件委托)
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><ul id="list"><li>项目1 <button class="delete-btn">删除</button></li><li>项目2 <button class="delete-btn">删除</button></li><li>项目3 <button class="delete-btn">删除</button></li><!-- 可能还有更多项目 --></ul><script>const list = document.getElementById('list');list.addEventListener('click', function(event) {// 利用事件冒泡,判断点击目标是否是删除按钮if (event.target && event.target.classList.contains('delete-btn')) {const li = event.target.parentElement;li.remove();console.log('删除了一个项目(事件委托)');}});</script></body>
</html>
从上述案例我们可以发现,我们只绑定了一个事件监听(ul),但是当子元素(li)触发点击事件之后,我们并没有对子元素绑定事件处理,但是也能够进行删除操作,这是因为子元素的事件会沿着DOM树向上传播,当传播到ul节点的时候触发了ul的事件处理机制。所以进行了删除。
5.如何阻止事件冒泡
当我们希望事件只在某个特定元素上响应,而不影响其父元素或祖先元素时,可以使用 event.stopPropagation() 来阻止事件冒泡。
此外,如果希望不仅阻止事件冒泡,还阻止当前元素上后续的同类型事件监听器执行,可以使用 event.stopImmediatePropagation()。
区别:
event.stopPropagation():阻止事件继续冒泡到父元素,但当前元素上绑定的其他同类型事件监听器仍会执行。event.stopImmediatePropagation():阻止事件冒泡,并且阻止当前元素上后续同类型事件监听器的执行。
案例:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8" /><title>阻止事件冒泡案例</title>
</head>
<body><div id="outer" style="padding:20px; border:1px solid #ccc;">外层容器(点击会触发)<button id="inner">点击我</button></div><script>const outer = document.getElementById('outer');const inner = document.getElementById('inner');outer.addEventListener('click', () => {console.log('【外层容器】点击事件触发');});// 监听器1:使用 stopPropagation()inner.addEventListener('click', (event) => {event.stopPropagation(); // 阻止事件冒泡,父元素的点击事件不会触发console.log('【按钮监听器1】点击事件触发,事件冒泡被阻止,父元素不会收到事件');});// 监听器2:同一元素上的另一个监听器inner.addEventListener('click', () => {console.log('【按钮监听器2】点击事件触发,仍然执行');});/*// 如果改为使用 stopImmediatePropagation(),效果如下:inner.addEventListener('click', (event) => {event.stopImmediatePropagation(); // 阻止事件冒泡,且阻止后续监听器执行console.log('【按钮监听器1】点击事件触发,事件冒泡和后续监听器执行被阻止,父元素不会收到事件');});inner.addEventListener('click', () => {console.log('【按钮监听器2】不会执行');});*/</script>
</body>
</html>
使用 stopPropagation() 时:
点击按钮会输出:
【按钮监听器1】点击事件触发,事件冒泡被阻止,父元素不会收到事件
【按钮监听器2】点击事件触发,仍然执行
使用 stopImmediatePropagation() 时:
点击按钮只会输出:
【按钮监听器1】点击事件触发,事件冒泡和后续监听器执行被阻止,父元素不会收到事件
6.总结
-
JavaScript事件机制包括三个主要阶段:
- 捕获阶段:事件从document节点向目标元素逐层传递,监听器在此阶段触发需设置
capture为true。 - 目标阶段:事件到达目标元素,触发绑定在目标元素上的事件监听器。
- 冒泡阶段:事件从目标元素向上传播到document,触发沿途元素绑定的冒泡监听器。
- 捕获阶段:事件从document节点向目标元素逐层传递,监听器在此阶段触发需设置
-
事件冒泡机制允许我们通过事件委托,只在父元素上绑定事件监听器,利用事件冒泡捕获子元素事件,极大优化性能和代码维护。
-
在需要阻止事件传播时,可使用event.stopPropagation()阻止冒泡,避免父元素响应事件;使用event.stopImmediatePropagation()则进一步阻止当前元素上后续同类型监听器执行。
-
事件监听器默认在目标阶段和冒泡阶段触发,捕获阶段监听需要显式开启
