事件流描述了事件在 DOM 树中传播的顺序。理解事件流是掌握事件处理的关键。
1. 基本概念
事件流三阶段
markdown
// 事件传播的完整流程
1. 捕获阶段 (Capturing Phase): 从上往下
2. 目标阶段 (Target Phase): 到达目标元素
3. 冒泡阶段 (Bubbling Phase): 从下往上
示例 DOM 结构
js
<div id="grandparent" style="padding: 50px; background-color: #f0f0f0;">
Grandparent
<div id="parent" style="padding: 30px; background-color: #e0e0e0;">
Parent
<button id="child" style="padding: 20px; background-color: #d0d0d0;">
Click me!
</button>
</div>
</div>
js
// JavaScript
const grandparent = document.getElementById('grandparent');
const parent = document.getElementById('parent');
const child = document.getElementById('child');
2. 捕获阶段 (Capturing Phase)
特点
- 从 window → document → ... → 目标元素
- 自上而下传播
- 默认不监听此阶段
使用方法
js
// 在 addEventListener 的第三个参数设置为 true
grandparent.addEventListener('click', function() {
console.log('Grandparent: 捕获阶段');
}, true); // true 表示在捕获阶段处理
parent.addEventListener('click', function() {
console.log('Parent: 捕获阶段');
}, true);
child.addEventListener('click', function() {
console.log('Child: 捕获阶段');
}, true);
3. 冒泡阶段 (Bubbling Phase)
特点
- 从目标元素 → ... → document → window
- 自下而上传播
- 默认行为(第三个参数为 false 或省略)
使用方法
js
grandparent.addEventListener('click', function() {
console.log('Grandparent: 冒泡阶段');
}, false); // false 或不指定表示冒泡阶段
parent.addEventListener('click', function() {
console.log('Parent: 冒泡阶段');
});
child.addEventListener('click', function() {
console.log('Child: 冒泡阶段');
});
4. 完整事件流演示
js
// 清理之前的监听器
function clearAllListeners() {
const listeners = [];
return function addListener(element, handler, useCapture) {
element.addEventListener('click', handler, useCapture);
listeners.push({ element, handler, useCapture });
};
}
// 添加完整事件监听
const listener = clearAllListeners();
// 捕获阶段
listener(grandparent, function() {
console.log('1. Grandparent: 捕获阶段');
}, true);
listener(parent, function() {
console.log('2. Parent: 捕获阶段');
}, true);
listener(child, function(e) {
console.log('3. Child: 目标阶段');
}, true);
// 冒泡阶段
listener(child, function(e) {
console.log('4. Child: 目标阶段');
}, false);
listener(parent, function() {
console.log('5. Parent: 冒泡阶段');
}, false);
listener(grandparent, function() {
console.log('6. Grandparent: 冒泡阶段');
}, false);
// 点击按钮输出:
// 1. Grandparent: 捕获阶段
// 2. Parent: 捕获阶段
// 3. Child: 目标阶段
// 4. Child: 目标阶段
// 5. Parent: 冒泡阶段
// 6. Grandparent: 冒泡阶段
5. 事件对象
event 对象
js
child.addEventListener('click', function(event) {
console.log('事件对象属性:');
console.log('event.target:', event.target); // 实际触发的元素
console.log('event.currentTarget:', event.currentTarget); // 当前处理元素
console.log('event.eventPhase:', event.eventPhase); // 当前阶段
// 1: 捕获, 2: 目标, 3: 冒泡
}, false);
6. 控制事件传播
6.1 event.stopPropagation()
阻止事件继续传播
js
grandparent.addEventListener('click', function(e) {
console.log('Grandparent: 捕获阶段');
}, true);
parent.addEventListener('click', function(e) {
console.log('Parent: 捕获阶段');
e.stopPropagation(); // 停止传播
}, true);
child.addEventListener('click', function(e) {
console.log('Child: 永远执行不到这里');
}, true);
// 点击 child 输出:
// Grandparent: 捕获阶段
// Parent: 捕获阶段
// 停止传播,后续事件不会执行
6.2 event.stopImmediatePropagation()
阻止事件传播,并阻止同一元素的其他监听器
js
function handler1() {
console.log('handler1');
}
function handler2() {
console.log('handler2');
}
function handler3() {
console.log('handler3 执行前停止');
event.stopImmediatePropagation();
}
function handler4() {
console.log('handler4 不会执行');
}
child.addEventListener('click', handler1);
child.addEventListener('click', handler2);
child.addEventListener('click', handler3);
child.addEventListener('click', handler4);
// 点击 child 输出:
// handler1
// handler2
// handler3 执行前停止
// handler4 不会执行
6.3 event.preventDefault()
阻止默认行为
js
document.getElementById('myLink').addEventListener('click', function(e) {
e.preventDefault(); // 阻止链接跳转
console.log('链接被点击,但不会跳转');
});
form.addEventListener('submit', function(e) {
e.preventDefault(); // 阻止表单提交
console.log('表单提交被阻止');
});
7. 事件委托 (Event Delegation)
7.1 使用冒泡机制
js
<ul id="todoList">
<li>任务1 <button class="delete">删除</button></li>
<li>任务2 <button class="delete">删除</button></li>
<li>任务3 <button class="delete">删除</button></li>
<li>任务4 <button class="delete">删除</button></li>
</ul>
js
// ❌ 低效的方法:为每个按钮添加监听器
const buttons = document.querySelectorAll('.delete');
buttons.forEach(button => {
button.addEventListener('click', function() {
this.parentElement.remove();
});
});
// ✅ 高效的事件委托
const todoList = document.getElementById('todoList');
todoList.addEventListener('click', function(event) {
if (event.target.classList.contains('delete')) {
event.target.parentElement.remove();
}
});
7.2 动态添加元素
js
// 添加新任务
function addNewTask() {
const newLi = document.createElement('li');
newLi.innerHTML = `新任务 <button class="delete">删除</button>`;
todoList.appendChild(newLi);
// 无需为新按钮添加事件监听
}
8. 实际应用示例
示例1:嵌套菜单
js
<div class="menu">
<button class="menu-toggle">菜单1</button>
<div class="submenu">
<a href="#">选项1</a>
<a href="#">选项2</a>
<a href="#">选项3</a>
</div>
</div>
<div class="menu">
<button class="menu-toggle">菜单2</button>
<div class="submenu">
<a href="#">选项A</a>
<a href="#">选项B</a>
</div>
</div>
js
// 事件委托处理所有菜单
document.addEventListener('click', function(event) {
const target = event.target;
if (target.classList.contains('menu-toggle')) {
// 切换菜单
const submenu = target.nextElementSibling;
submenu.style.display = submenu.style.display === 'block' ? 'none' : 'block';
} else if (target.tagName === 'A') {
// 处理菜单项点击
console.log('选择了:', target.textContent);
} else {
// 点击外部,关闭所有菜单
document.querySelectorAll('.submenu').forEach(menu => {
menu.style.display = 'none';
});
}
});
示例2:模态框
js
// 事件委托实现点击外部关闭
document.addEventListener('click', function(event) {
const modal = document.getElementById('modal');
const closeBtn = document.getElementById('closeModal');
const openBtn = document.getElementById('openModal');
if (event.target === openBtn) {
modal.style.display = 'block';
} else if (event.target === closeBtn || event.target === modal) {
modal.style.display = 'none';
} else if (modal.style.display === 'block') {
// 防止冒泡
event.stopPropagation();
}
});
// 阻止模态框内容点击关闭
document.getElementById('modalContent').addEventListener('click', function(event) {
event.stopPropagation();
});
9. 性能优化
9.1 减少事件监听器数量
js
// ❌ 性能差:每个元素都添加监听器
document.querySelectorAll('.item').forEach(item => {
item.addEventListener('click', handleClick);
});
// ✅ 性能好:一个父元素监听
document.getElementById('container').addEventListener('click', function(event) {
if (event.target.classList.contains('item')) {
handleClick(event);
}
});
// 或者使用 closest
document.addEventListener('click', function(event) {
const item = event.target.closest('.item');
if (item) {
handleClick(event, item);
}
});
9.2 防抖和节流
js
// 节流处理
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
func.apply(this, args);
}
};
}
// 防抖处理
function debounce(func, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// 使用
window.addEventListener('scroll', throttle(function() {
console.log('滚动事件(节流)');
}, 100));
window.addEventListener('resize', debounce(function() {
console.log('调整大小事件(防抖)');
}, 250));
10. 事件阶段常量
js
// 事件阶段常量
const Event = {
NONE: 0, // 无
CAPTURING_PHASE: 1, // 捕获
AT_TARGET: 2, // 目标
BUBBLING_PHASE: 3 // 冒泡
};
// 使用
element.addEventListener('click', function(event) {
switch(event.eventPhase) {
case Event.CAPTURING_PHASE:
console.log('捕获阶段');
break;
case Event.AT_TARGET:
console.log('目标阶段');
break;
case Event.BUBBLING_PHASE:
console.log('冒泡阶段');
break;
}
}, true);
11. 高级技巧
11.1 自定义事件
js
// 创建自定义事件
const customEvent = new CustomEvent('myEvent', {
bubbles: true, // 是否冒泡
cancelable: true, // 是否可取消
detail: { // 自定义数据
message: 'Hello World',
time: new Date()
}
});
// 监听自定义事件
document.addEventListener('myEvent', function(event) {
console.log('自定义事件触发:', event.detail);
console.log('是否冒泡:', event.bubbles);
console.log('目标元素:', event.target);
});
// 触发事件
setTimeout(() => {
document.dispatchEvent(customEvent);
}, 1000);
11.2 一次性事件监听
js
// 传统方法
let handled = false;
element.addEventListener('click', function handler(event) {
if (handled) return;
handled = true;
console.log('只执行一次');
element.removeEventListener('click', handler);
});
// 使用 { once: true }
element.addEventListener('click', function() {
console.log('只执行一次');
}, { once: true });
// 捕获阶段也适用
element.addEventListener('click', function() {
console.log('捕获阶段只执行一次');
}, { capture: true, once: true });
11.3 被动事件监听
js
// 提高滚动性能
document.addEventListener('wheel', function(event) {
// 这里不会调用 preventDefault
console.log('滚动事件');
}, { passive: true });
// 尝试调用会出错
document.addEventListener('wheel', function(event) {
// 在 passive 为 true 时,调用 preventDefault 会报错
// event.preventDefault(); // TypeError
}, { passive: true });
12. 常见问题
12.1 阻止默认行为和冒泡
js
document.getElementById('link').addEventListener('click', function(event) {
event.preventDefault(); // 阻止默认行为
event.stopPropagation(); // 阻止冒泡
console.log('链接被点击,但不会跳转,也不会冒泡');
});
// 在表单中使用
document.getElementById('form').addEventListener('submit', function(event) {
if (!isValid()) {
event.preventDefault();
event.stopPropagation();
return false;
}
});
12.2 移除事件监听器
js
function handleClick() {
console.log('点击事件');
}
// 添加
element.addEventListener('click', handleClick);
// 移除(必须使用相同的函数引用)
element.removeEventListener('click', handleClick);
// ❌ 错误:匿名函数无法移除
element.addEventListener('click', function() {
console.log('匿名函数');
});
element.removeEventListener('click', function() {
console.log('无法移除');
}); // 不生效
13. 现代框架中的事件处理
React
js
function MyComponent() {
const handleClick = (event) => {
console.log('React 事件是合成事件');
console.log('事件目标:', event.target);
console.log('冒泡行为:', event.nativeEvent.bubbles);
};
const handleSubmit = (event) => {
event.preventDefault(); // 阻止表单提交
console.log('表单提交被阻止');
};
return (
<div onClick={handleClick}>
<form onSubmit={handleSubmit}>
<button type="submit">提交</button>
</form>
</div>
);
}
Vue
js
<template>
<div @click="handleClick">
<form @submit.prevent="handleSubmit">
<button type="submit">提交</button>
</form>
</div>
</template>
<script>
export default {
methods: {
handleClick(event) {
console.log('Vue 事件处理');
event.stopPropagation(); // 原生方法
},
handleSubmit() {
console.log('.prevent 修饰符自动阻止默认行为');
}
}
}
</script>
14. 总结
关键点总结
| 特性 | 事件捕获 | 事件冒泡 |
|---|---|---|
| 传播方向 | 上 → 下 | 下 → 上 |
| 默认启用 | ❌ 需显式设置 | ✅ 默认 |
| 使用场景 | 少,特殊需求 | 多,事件委托 |
| 添加方法 | addEventListener(..., true) |
addEventListener(..., false) |
最佳实践
- 使用事件委托:减少监听器数量,提高性能
- 理解事件流:知道事件如何传播
- 合理使用停止传播:但不要滥用
- 使用被动监听器:提高滚动性能
- 注意内存泄漏:及时移除不需要的监听器
- 利用框架特性:React/Vue 等框架有优化
记忆口诀
html
事件三阶段,捕获、目标 和 冒泡
从上往下 叫捕获,从下往上 叫冒泡
目标在中间,两阶段都要到
委托用冒泡,性能高可靠
通过理解事件冒泡和捕获,你可以更有效地处理 DOM 事件,优化性能,并写出更优雅的事件处理代码。