一、核心概念通俗解释
比喻:快递包裹的派送过程
想象你网购了一个鼠标,从仓库到你家:
事件捕获阶段(从上往下):
京东仓库(window) → 北京分拣中心(document) → 朝阳区站点(body) → 你家小区(按钮)
事件目标阶段(到达目标):
快递员把鼠标交到你手里(按钮被点击)
事件冒泡阶段(从下往上):
你签收快递(按钮) → 小区代收点 → 朝阳区站点 → 北京分拣中心 → 京东仓库
二、事件传播的3个阶段
1. 事件捕获阶段(Capture Phase)
window → document → html → body → 父元素 → 目标元素
方向:从上到下(外层到内层)
2. 目标阶段(Target Phase)
目标元素(被点击的元素)
3. 事件冒泡阶段(Bubble Phase)
目标元素 → 父元素 → body → html → document → window
方向:从下到上(内层到外层)
三、代码示例演示
示例1:基本的事件传播
<!DOCTYPE html>
<html lang="zh-CN">
<body>
<div id="爷爷" style="padding: 50px; background: lightblue;">
<div id="爸爸" style="padding: 30px; background: lightgreen;">
<button id="儿子" style="padding: 20px;">点我!</button>
</div>
</div>
<script>
// 获取元素
const 爷爷 = document.getElementById('爷爷');
const 爸爸 = document.getElementById('爸爸');
const 儿子 = document.getElementById('儿子');
// 默认是冒泡阶段(false)
爷爷.addEventListener('click', () => {
console.log('爷爷被点击了(冒泡阶段)');
});
爸爸.addEventListener('click', () => {
console.log('爸爸被点击了(冒泡阶段)');
});
儿子.addEventListener('click', () => {
console.log('儿子被点击了(目标阶段)');
});
// 添加捕获阶段的监听(true)
爷爷.addEventListener('click', () => {
console.log('爷爷被点击了(捕获阶段)');
}, true); // 第三个参数为 true 表示捕获阶段
爸爸.addEventListener('click', () => {
console.log('爸爸被点击了(捕获阶段)');
}, true);
</script>
</body>
</html>
点击按钮时的输出顺序:
爷爷被点击了(捕获阶段) ← 从上往下
爸爸被点击了(捕获阶段) ← 从上往下
儿子被点击了(目标阶段) ← 到达目标
爸爸被点击了(冒泡阶段) ← 从下往上
爷爷被点击了(冒泡阶段) ← 从下往上
四、stopPropagation()的作用
比喻:疫情隔离
假设你家小区(按钮)有人确诊了(被点击),疫情传播路径是:
你家 → 整个小区 → 整个街道 → 整个城市
stopPropagation()的作用:在你家门口拉起警戒线,阻止疫情扩散
示例2:使用 stopPropagation()
<script>
爷爷.addEventListener('click', (e) => {
console.log('爷爷:我要向上汇报!');
});
爸爸.addEventListener('click', (e) => {
console.log('爸爸:我要向上汇报!');
});
儿子.addEventListener('click', (e) => {
console.log('儿子:我是被点击的按钮');
e.stopPropagation(); // 🚨 关键:阻止事件继续传播!
console.log('儿子:我不让爸爸和爷爷知道!');
});
</script>
点击按钮时的输出:
儿子:我是被点击的按钮
儿子:我不让爸爸和爷爷知道!
解释:事件在"儿子"这里就被拦截了,不会继续冒泡到"爸爸"和"爷爷"
五、addEventListener的第三个参数
三种使用方式:
方式1:默认(冒泡阶段)
元素.addEventListener('click', 处理函数);
// 或
元素.addEventListener('click', 处理函数, false);
方式2:捕获阶段
元素.addEventListener('click', 处理函数, true);
方式3:配置对象(现代写法)
元素.addEventListener('click', 处理函数, {
capture: true, // 是否在捕获阶段触发
once: true, // 只触发一次
passive: true // 不会调用 preventDefault()
});
六、实际应用场景
场景1:模态框(阻止点击外部关闭)
<div class="modal-overlay" id="modal">
<div class="modal-content">
<h2>重要通知</h2>
<p>这是一个模态框</p>
<button class="close-btn">关闭</button>
</div>
</div>
<script>
const modal = document.getElementById('modal');
const closeBtn = document.querySelector('.close-btn');
// 点击遮罩层关闭模态框
modal.addEventListener('click', (e) => {
if (e.target === modal) { // 点击的是遮罩层,不是内容
closeModal();
}
});
// 点击关闭按钮
closeBtn.addEventListener('click', (e) => {
e.stopPropagation(); // 🚨 阻止事件冒泡到遮罩层
closeModal();
});
function closeModal() {
modal.style.display = 'none';
}
</script>
场景2:下拉菜单
<div class="dropdown" id="dropdown">
<button class="dropdown-btn">选择选项</button>
<ul class="dropdown-menu">
<li><a href="#" class="option">选项1</a></li>
<li><a href="#" class="option">选项2</a></li>
<li><a href="#" class="option">选项3</a></li>
</ul>
</div>
<script>
const dropdown = document.getElementById('dropdown');
// 点击下拉按钮
document.querySelector('.dropdown-btn').addEventListener('click', (e) => {
e.stopPropagation(); // 阻止冒泡到document
dropdown.classList.toggle('open');
});
// 点击菜单项
document.querySelectorAll('.option').forEach(item => {
item.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止冒泡
console.log('选择了:', e.target.textContent);
});
});
// 点击页面其他地方关闭下拉菜单
document.addEventListener('click', () => {
dropdown.classList.remove('open');
});
</script>
场景3:事件委托
<ul id="todo-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 todoList = document.getElementById('todo-list');
// ❌ 不好的做法:给每个按钮单独加事件
// document.querySelectorAll('.delete-btn').forEach(btn => {
// btn.addEventListener('click', () => { /* ... */ });
// });
// ✅ 好的做法:事件委托
todoList.addEventListener('click', (e) => {
// 检查点击的是否是删除按钮
if (e.target.classList.contains('delete-btn')) {
e.stopPropagation(); // 阻止事件冒泡到更高层
const li = e.target.closest('li');
li.remove();
console.log('删除了任务');
}
// 可以继续处理其他类型的点击
if (e.target.tagName === 'LI') {
console.log('点击了任务文本');
}
});
</script>
七、事件传播可视化演示
<!DOCTYPE html>
<html>
<style>
#家族树 { padding: 20px; }
.代 {
padding: 20px; margin: 10px;
border: 2px solid; cursor: pointer;
}
#爷爷 { background: #e3f2fd; }
#爸爸 { background: #f3e5f5; }
#儿子 { background: #e8f5e8; }
.console {
background: #000; color: #0f0;
padding: 10px; font-family: monospace;
height: 200px; overflow-y: auto;
}
</style>
<body>
<div id="家族树">
<div id="爷爷" class="代">爷爷
<div id="爸爸" class="代">爸爸
<div id="儿子" class="代">儿子</div>
</div>
</div>
</div>
<div class="console" id="log"></div>
<script>
const 日志 = document.getElementById('log');
function 记录(消息) {
日志.innerHTML += 消息 + '<br>';
日志.scrollTop = 日志.scrollHeight;
}
const 家族成员 = ['爷爷', '爸爸', '儿子'];
// 为每个成员添加捕获和冒泡事件
家族成员.forEach(成员 => {
const 元素 = document.getElementById(成员);
// 捕获阶段
元素.addEventListener('click', (e) => {
记录(`捕获阶段:${成员} 感受到了点击`);
}, true);
// 冒泡阶段
元素.addEventListener('click', (e) => {
记录(`冒泡阶段:${成员} 在向上报告`);
}, false);
// 目标阶段(只有被点击的元素)
元素.addEventListener('click', (e) => {
if (e.currentTarget === e.target) {
记录(`目标阶段:${成员} 被直接点击了!`);
}
});
});
// 添加阻止传播按钮
document.getElementById('儿子').addEventListener('click', (e) => {
if (e.shiftKey) { // 按住Shift键点击
e.stopPropagation();
记录('儿子:我阻止了事件传播!');
}
});
</script>
</body>
</html>
八、stopPropagation()的注意事项
1. 阻止传播 ≠ 阻止默认行为
元素.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡
e.preventDefault(); // 阻止默认行为(如链接跳转)
});
2. 阻止传播的副作用
<a href="https://baidu.com" id="link">
<span id="inner">点击我</span>
</a>
<script>
// ❌ 错误的用法
document.getElementById('inner').addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡到链接
console.log('点击了span');
// 但链接不会跳转!
});
// ✅ 正确的做法
document.getElementById('link').addEventListener('click', (e) => {
console.log('链接被点击了');
// 在这里决定是否跳转
});
</script>
九、总结表格
| 场景 | 解决方法 | 示例 |
|---|---|---|
| 阻止事件冒泡 | e.stopPropagation() | 下拉菜单点击时不关闭 |
| 事件委托 | 在父元素监听 | 动态列表项处理 |
| 只触发一次 | { once: true } | 第一次点击后不再监听 |
| 只在捕获阶段触发 | { capture: true } | 提前拦截事件 |
| 阻止默认行为 | e.preventDefault() | 阻止表单提交、链接跳转 |
十、一句话记忆
事件传播就像扔石头进池塘:
- 捕获阶段:石头从上往下落(
window → 目标) - 目标阶段:石头击中水面(
目标元素) - 冒泡阶段:涟漪从中心往外扩散(
目标 → window)
stopPropagation() 就是在水面铺一层防扩散膜,阻止涟漪继续扩散!