什么是事件冒泡 (Bubbling) 和事件捕获 (Capturing)?
事件冒泡 (Event Bubbling) 和 事件捕获 (Event Capturing) 是 JavaScript 中处理 DOM 事件流(Event Flow)的两种机制。它们描述了当你在页面上触发一个事件(例如点击一个按钮)时,这个事件如何在 DOM 树的元素之间传递。
为了理解这两个概念,首先要明白 DOM 是树状结构的。当你点击一个子元素时,你实际上也点击了它的父元素、祖父元素,直到 window 对象。
1. 核心概念
事件捕获 (Capturing) —— “从上往下”
- 方向: 从 DOM 树的根节点(Root)向目标元素(Target)传播。
- 过程: 事件首先被最外层的元素(如
window或document)捕获,然后一层层向下传递,直到到达实际触发事件的目标元素。 - 比喻: 就像公司老板下达命令,先经过经理,再经过组长,最后传达到具体的员工。
事件冒泡 (Bubbling) —— “从下往上”
- 方向: 从目标元素(Target)向 DOM 树的根节点(Root)传播。
- 过程: 事件先在目标元素上触发,然后像水里的气泡一样,一层层向上冒,触发父元素、祖父元素的同类型事件,直到
window。 - 比喻: 就像员工有个诉求,先告诉组长,组长告诉经理,经理再汇报给老板。
- 注意: 这是现代浏览器处理事件的默认方式。
2. W3C 标准事件流
现代浏览器遵循 W3C 标准,将事件传播分为三个阶段:
- 捕获阶段 (Capturing Phase): 事件从 Window 向下传递到目标元素。
- 目标阶段 (Target Phase): 事件到达实际的目标元素。
- 冒泡阶段 (Bubbling Phase): 事件从目标元素向上回传到 Window。
3. 代码演示与控制
在 JavaScript 中,我们使用 addEventListener 来监听事件。它的第三个参数决定了监听器是在“捕获阶段”还是“冒泡阶段”触发。
javascript
element.addEventListener(event, function, useCapture);
useCapture(可选):false(默认值): 在 冒泡阶段 触发。true: 在 捕获阶段 触发。
示例场景
假设结构如下:div (父) > button (子)
html
<div id="parent">
<button id="child">点击我</button>
</div>
javascript
const parent = document.getElementById('parent');
const child = document.getElementById('child');
// 捕获阶段监听 (true)
parent.addEventListener('click', () => {
console.log('父元素 - 捕获阶段');
}, true);
child.addEventListener('click', () => {
console.log('子元素 - 捕获阶段');
}, true);
// 冒泡阶段监听 (false 或省略)
parent.addEventListener('click', () => {
console.log('父元素 - 冒泡阶段');
}, false);
child.addEventListener('click', () => {
console.log('子元素 - 冒泡阶段');
}, false);
当你点击按钮时,控制台输出顺序如下:
父元素 - 捕获阶段(事件从顶层下来,先遇到父元素)子元素 - 捕获阶段(事件到达目标)子元素 - 冒泡阶段(事件开始往上冒)父元素 - 冒泡阶段(事件冒泡到父元素)
4. 阻止传播 (stopPropagation)
有时候我们不希望事件继续传播(例如:点击弹窗内的按钮,不希望触发弹窗背景的关闭事件)。我们可以使用 event.stopPropagation()。
- 如果在 捕获 阶段调用:事件将停止向下传递,目标元素可能接收不到事件。
- 如果在 冒泡 阶段调用:事件触发当前元素后,不会再向上传递给父元素。
javascript
child.addEventListener('click', (event) => {
event.stopPropagation(); // 阻止冒泡
console.log('子元素被点击,父元素不会收到通知');
});
5. 实际应用:事件委托 (Event Delegation)
事件委托是利用 事件冒泡 机制的最常见应用。
场景: 你有一个列表 <ul>,里面有 1000 个 <li>。
笨办法: 给每个 <li> 都绑定一个监听器(消耗内存,性能差)。
好办法(事件委托): 给父元素 <ul> 绑定一个监听器。
当用户点击 <li> 时,事件会冒泡到 <ul>。我们在 <ul> 的监听器里通过 event.target 就能知道具体点击了哪个 <li>。
javascript
document.getElementById('myList').addEventListener('click', function(e) {
// e.target 是实际被点击的元素
if (e.target && e.target.nodeName === 'LI') {
console.log('List item clicked!', e.target.textContent);
}
});
总结
| 特性 | 事件捕获 (Capturing) | 事件冒泡 (Bubbling) |
|---|---|---|
| 方向 | 外 内 (Top-Down) | 内 外 (Bottom-Up) |
| 默认状态 | 需要显式开启 (true) |
addEventListener 的默认行为 |
| 主要用途 | 较少使用,用于在事件到达目标前拦截 | 非常常用,用于事件委托 |