捕获和冒泡在事件流监听中的出现和防范

7 阅读3分钟

在监听器中我们常用到e.target:

e.target 表示触发该事件的 DOM 元素(即用户实际点击的那个元素,而不是绑定监听器的元素——这点在事件冒泡中特别重要)”

这个触及了 JavaScript 事件机制的核心。下面我们从事件流模型出发,深入分析:


一、什么是事件冒泡(Event Bubbling)?

✅ 定义:

当一个元素上的事件被触发时,该事件会从最内层(目标元素)开始,逐级向上传播到其祖先元素,直到 documentwindow

🌰 示例:

<div id="grandparent">
  <div id="parent">
    <button id="child">Click me</button>
  </div>
</div>

<script>
  document.getElementById("grandparent").addEventListener("click", (e) => {
    console.log("Grandparent clicked, target:", e.target.id);
  });
  document.getElementById("parent").addEventListener("click", (e) => {
    console.log("Parent clicked, target:", e.target.id);
  });
  document.getElementById("child").addEventListener("click", (e) => {
    console.log("Child clicked, target:", e.target.id);
  });
</script>

👉 点击按钮时,控制台输出:

Child clicked, target: child
Parent clicked, target: child    注意!target 仍是 child
Grandparent clicked, target: child

✅ 关键点:

  • e.target 始终是最初被点击的元素(#child
  • thise.currentTarget 才是当前处理函数绑定的元素

🔍 e.currentTarget === 绑定监听器的那个元素
e.target === 用户真正点击的那个元素


二、与“冒泡”类似的机制:事件捕获(Event Capturing)

✅ 定义:

事件从最外层(如 window)开始,逐级向下传递到目标元素。这是事件流的第一阶段

📌 W3C 标准事件流三阶段:

  1. 捕获阶段(Capturing):从 windowdocument → ... → 父元素 → 目标
  2. 目标阶段(Target):事件到达目标元素
  3. 冒泡阶段(Bubbling):从目标 → 父元素 → ... → documentwindow

默认情况下,addEventListener 监听的是冒泡阶段

如何启用捕获?

element.addEventListener("click", handler, true); // 第三个参数为 true 表示捕获阶段

捕获 vs 冒泡 对比:

特性事件捕获事件冒泡
传播方向外 → 内内 → 外
默认行为❌ 不默认触发✅ 默认触发
使用频率较低(特殊场景)极高(事件委托依赖它)
e.target仍是目标元素仍是目标元素

⚠️ 无论捕获还是冒泡,e.target 始终指向最初触发事件的元素


三、如何“避免”冒泡或捕获?

你不能完全“禁止”事件流(因为它是浏览器标准),但可以阻止其继续传播

1. 阻止冒泡(最常用)

e.stopPropagation(); // 阻止事件继续向上冒泡

2. 阻止捕获(同上,在捕获阶段调用即可)

// 在捕获阶段的监听器中
element.addEventListener(
  "click",
  (e) => {
    e.stopPropagation(); // 阻止继续向下捕获
  },
  true,
);

3. 阻止所有后续处理(包括同级监听器)

e.stopImmediatePropagation(); // 不仅阻止冒泡,还阻止同一元素上其他监听器执行

四、典型应用场景与避坑指南

✅ 场景1:模态框点击外部关闭

modal.addEventListener("click", (e) => {
  if (e.target === modal) {
    // 只有点击 modal 背景才关闭
    closeModal();
  }
});

利用 e.target 判断是否点在了遮罩层本身,而不是内部内容。

✅ 场景2:事件委托 + 阻止冒泡

<ul id="list">
  <li><button class="delete" data-id="1">删除</button></li>
</ul>
list.addEventListener("click", (e) => {
  if (e.target.classList.contains("delete")) {
    e.stopPropagation(); // 防止触发 li 或 ul 的其他点击逻辑
    handleDelete(e.target.dataset.id);
  }
});

❌ 常见错误:

  • 误以为 this 是点击的元素(其实是绑定监听器的元素)
  • 忘记阻止冒泡,导致父级逻辑被意外触发

五、总结

概念说明
e.target永远是用户实际点击的元素,与事件流阶段无关
事件冒泡事件从子元素向父元素传播(默认行为)
事件捕获事件从父元素向子元素传播(需显式开启)
如何阻止e.stopPropagation() 阻止继续传播
关键区别传播方向不同,但 e.target 始终不变

💡 实际开发中,90% 以上使用冒泡 + 事件委托;捕获仅用于极少数需要“提前拦截”的场景(如全局日志、权限校验等)。