源本科技 | 码上会

JavaScript 事件冒泡

2026/01/12
11
0

学习目标

  • 理解事件冒泡的传播机制与执行顺序

  • 掌握 stopPropagation() 的作用与使用场景

  • 能够利用事件冒泡实现高效的事件委托(Event Delegation)

  • 区分事件冒泡与事件捕获的差异

  • 在实际开发中合理控制事件流,避免意外行为


什么是事件冒泡

事件冒泡(Event Bubbling) 是 DOM 事件传播的一种机制:当某个子元素上的事件被触发时,该事件会从目标元素开始,逐级向上传播到其祖先元素,直至 documentwindow

  • 默认启用:JavaScript 中所有事件监听器默认在冒泡阶段执行。

  • 传播方向子元素 → 父元素 → 祖先元素 → ... → document

  • 共享事件对象:每个监听器都能访问同一个 event 对象,包含 targetcurrentTarget 等信息。


事件冒泡示例

<div class="grandparent" id="one">Grandparent
  <div class="parent" id="two">Parent
    <div class="child" id="three">Child</div>
  </div>
</div>

<script>
  const grandparent = document.getElementById('one');
  const parent = document.getElementById('two');
  const child = document.getElementById('three');

  grandparent.addEventListener('click', () => console.log("Grandparent Clicked"));
  parent.addEventListener('click', () => console.log("Parent Clicked"));
  child.addEventListener('click', () => console.log("Child Clicked"));
</script>

点击最内层 .child 元素时的输出:

Child Clicked
Parent Clicked
Grandparent Clicked

执行流程:

  1. 用户点击 .child(目标元素)

  2. 触发 .child 上的监听器 → 输出 "Child Clicked"

  3. 事件冒泡至 .parent → 执行其监听器 → 输出 "Parent Clicked"

  4. 继续冒泡至 .grandparent → 输出 "Grandparent Clicked"

即使只点击了最内层元素,所有祖先元素的点击监听器都会被触发!


阻止事件冒泡

有时我们希望仅目标元素响应事件,不希望事件继续向上传播。此时可使用 event.stopPropagation()

child.addEventListener('click', (e) => {
  e.stopPropagation(); // 阻止冒泡
  console.log("Child Clicked");
});
  • 点击 .child 时,只有 "Child Clicked" 被打印

  • .parent.grandparent 的监听器不会执行

典型应用场景

  • 点击模态框内部时不关闭弹窗

  • 点击下拉菜单选项时不触发外层容器的隐藏逻辑


事件冒泡 vs 事件捕获

DOM 事件流分为三个阶段:

  • 捕获阶段 -> 目标阶段 -> 冒泡阶段

特性

事件冒泡(Bubbling)

事件捕获(Capturing)

传播方向

子 → 父 → 根

根 → 父 → 子

默认行为

✅ 默认启用

❌ 需显式开启

监听器注册方式

addEventListener('click', handler)

addEventListener('click', handler, true)

执行顺序

子元素先执行

祖先元素先执行

使用频率

高(尤其用于事件委托)

低(特殊场景)

示例:启用捕获阶段

parent.addEventListener('click', () => {
  console.log("Parent (捕获)");
}, true); // 第三个参数为 true 表示在捕获阶段监听

child.addEventListener('click', () => {
  console.log("Child (冒泡)");
}); // 默认冒泡阶段

点击 .child 时输出:

Parent (捕获)
Child (冒泡)

实战应用:事件委托

Event Delegation

利用事件冒泡,我们可以在父元素上统一处理多个子元素的事件,而无需为每个子元素单独绑定监听器。

优势:

  • 减少内存占用(监听器数量大幅减少)

  • 自动支持动态添加的子元素

  • 代码更简洁、易维护

示例:处理列表项点击

<ul id="list">
  <li data-id="1">项目 1</li>
  <li data-id="2">项目 2</li>
  <li data-id="3">项目 3</li>
</ul>

<script>
  document.getElementById('list').addEventListener('click', (e) => {
    if (e.target.tagName === 'LI') {
      console.log('点击了项目:', e.target.dataset.id);
    }
  });
</script>

即使后续通过 JavaScript 动态添加新的 <li>,点击事件依然能被正确处理!


最佳实践

建议

说明

优先使用事件委托

尤其适用于表格、列表、按钮组等重复结构

谨慎使用 stopPropagation()

过度使用可能破坏组件间的正常交互(如阻止表单提交)

区分 targetcurrentTarget

e.target 是实际触发元素,e.currentTarget 是监听器绑定的元素

避免在冒泡中修改 DOM 结构

可能导致事件流异常或性能问题

结合 CSS pointer-events: none

可临时禁用某区域的事件响应,作为 stopPropagation 的补充方案


重点总结

  • 事件冒泡是 从子到父 的事件传播机制,默认开启。

  • 执行顺序:目标元素 → 父元素 → 祖先元素

  • 使用 event.stopPropagation()中断冒泡,防止祖先监听器被触发。

  • 事件委托是事件冒泡最强大的应用,提升性能与可维护性。

  • 事件流包含 捕获 → 目标 → 冒泡 三个阶段,冒泡最常用。

  • 合理控制事件流,是构建健壮交互系统的关键。


思考题

  1. 如果同时为同一个元素注册了捕获阶段和冒泡阶段的点击监听器,它们的执行顺序是怎样的?

  2. 在一个复杂的 UI 组件(如日期选择器)中,如何利用 stopPropagation() 防止点击内部元素时意外关闭整个组件?

  3. 为什么说事件委托特别适合处理“动态生成的内容”?请结合 e.target 的特性解释。