JavaScript 事件冒泡与事件捕获
1. 什么是事件流?
当浏览器中的某个元素触发了一个事件(比如 click
),这个事件并不是只在触发元素上执行一次,而是会按照特定的顺序在不同的 DOM 节点之间传播,这个传播过程就是事件流。
DOM 事件流分为三个阶段:
- 事件捕获阶段(Capture Phase)
- 目标阶段(Target Phase)
- 事件冒泡阶段(Bubbling Phase)
注意:在旧版本 IE(IE8 及以下)中,只支持事件冒泡,不支持事件捕获。
2. 事件捕获(Event Capture)
- 方向:从最外层的祖先元素(通常是
window
)向内传播,直到目标元素。 - 触发顺序:先触发父级元素的事件处理程序,再到子级。
- 语法:在
addEventListener
的第三个参数设为true
时,表示在捕获阶段执行。
<div id="grandparent" style="padding:20px; background:red;">祖父<div id="parent" style="padding:20px; background:green;">父亲<div id="child" style="padding:20px; background:blue;">儿子</div></div>
</div><script>const grandparent = document.getElementById('grandparent');const parent = document.getElementById('parent');const child = document.getElementById('child');grandparent.addEventListener('click', () => {console.log('捕获阶段:祖父');}, true);parent.addEventListener('click', () => {console.log('捕获阶段:父亲');}, true);child.addEventListener('click', () => {console.log('捕获阶段:儿子');}, true);
</script>
点击 “儿子” 元素的输出:
原因:事件从最外层(祖父)向目标(儿子)传播
3. 事件冒泡(Event Bubbling)
- 方向:从目标元素向外传播,逐级向上,直到
window
。 - 触发顺序:先触发子级元素的事件处理程序,再到父级。
- 语法:
addEventListener
默认(第三个参数为false
)就是在冒泡阶段执行。
<div id="grandparent" style="padding:20px; background:red;">祖父<div id="parent" style="padding:20px; background:green;">父亲<div id="child" style="padding:20px; background:blue;">儿子</div></div>
</div><script>const grandparent = document.getElementById('grandparent');const parent = document.getElementById('parent');const child = document.getElementById('child');grandparent.addEventListener('click', () => {console.log('冒泡阶段:祖父');}, false);parent.addEventListener('click', () => {console.log('冒泡阶段:父亲');}, false);child.addEventListener('click', () => {console.log('冒泡阶段:儿子');}, false);
</script>
点击 “儿子” 元素的输出:
原因:事件从目标(儿子)向父级(父亲、祖父)传播。
4. 完整事件流顺序
结合捕获和冒泡,完整的触发顺序是:
- 捕获阶段:从
window
→document
→html
→ ... → 目标元素 - 目标阶段:事件在目标元素上触发
- 冒泡阶段:从目标元素 → ... →
html
→document
→window
<div id="outer" style="padding:20px; background:yellow;">外层<div id="inner" style="padding:20px; background:orange;">内层</div>
</div><script>const outer = document.getElementById('outer');const inner = document.getElementById('inner');outer.addEventListener('click', () => console.log('捕获:outer'), true);inner.addEventListener('click', () => console.log('捕获:inner'), true);outer.addEventListener('click', () => console.log('冒泡:outer'), false);inner.addEventListener('click', () => console.log('冒泡:inner'), false);
</script>
点击 “内层” 输出:
5. 如何阻止事件冒泡?
在实际开发中,有时我们不想让事件继续向上传播(比如点击按钮时不想触发父级的点击事件),可以使用:
方法 1:event.stopPropagation()
<div id="parent" style="padding:20px; background:green;">父亲<button id="btn">点我</button>
</div><script>const parent = document.getElementById('parent');const btn = document.getElementById('btn');parent.addEventListener('click', () => {console.log('父元素被点击');});btn.addEventListener('click', (e) => {console.log('按钮被点击');e.stopPropagation(); // 阻止事件冒泡});
</script>
点击按钮输出:
不会触发父元素的点击事件。
方法 2:event.stopImmediatePropagation()
- 不仅阻止事件冒泡,还会阻止当前元素后面注册的相同类型事件的执行。
<button id="btn">按钮</button><script>const btn = document.getElementById('btn');btn.addEventListener('click', (e) => {console.log('第一个点击事件');e.stopImmediatePropagation();});btn.addEventListener('click', () => {console.log('第二个点击事件'); // 不会执行});
</script>
点击按钮输出:
6. 实际开发中的应用场景
- 事件委托(Event Delegation)利用事件冒泡,将子元素的事件监听委托给父元素,减少监听器数量,提高性能。
- 防止父级元素事件被误触发例如点击关闭按钮时,避免触发整个弹窗的点击事件。
- 自定义组件事件控制控制组件内部事件是否向外传播。
. 总结
- 事件捕获:从外向内传播,
addEventListener(..., true)
- 事件冒泡:从内向外传播,
addEventListener(..., false)
(默认) - 阻止冒泡:
event.stopPropagation()
- 阻止冒泡 + 阻止后续同类型事件:
event.stopImmediatePropagation()
7.事件捕获与冒泡的完整流程图
假设我们有三层嵌套元素:
完整事件流顺序图
图解说明
捕获阶段(Capture Phase)
- 事件从最顶层的
window
对象开始向下传播 - 依次经过
document
→html
→body
→ ... → 目标元素的父级 - 在这个阶段注册的事件监听器(
addEventListener(..., true)
)会被触发
- 事件从最顶层的
目标阶段(Target Phase)
- 事件到达实际触发的元素(目标元素)
- 在这个阶段,无论捕获还是冒泡的监听器都会按照注册顺序执行
冒泡阶段(Bubbling Phase)
- 事件从目标元素向上传播
- 依次经过父级 → ... →
body
→html
→document
→window
- 在这个阶段注册的事件监听器(
addEventListener(..., false)
或默认)会被触发