JavaScript 事件流:从“捕获”到“冒泡”的完整旅程

🌊 JavaScript 事件流:从"捕获"到"冒泡"的完整旅程

当你点击页面上的一个按钮时,这个点击动作并不是瞬间只在按钮上发生的。

在浏览器的世界里,这个点击事件像一阵风,经历了一场从上到下 ,再从下到上的完整旅行。

这就是 DOM 事件流(Event Flow)

📂 目录

  1. [🤔 什么是事件流?](#🤔 什么是事件流?)
  2. [🎬 事件流的三个阶段](#🎬 事件流的三个阶段)
  3. [💻 代码实战:如何控制捕获与冒泡?](#💻 代码实战:如何控制捕获与冒泡?)
  4. [🛑 如何阻止事件传播?](#🛑 如何阻止事件传播?)
  5. [🚀 实际应用场景:为什么我们需要它们?](#🚀 实际应用场景:为什么我们需要它们?)
  6. [💡 总结](#💡 总结)

1. 🤔 什么是事件流?

定义

事件流描述的是从页面中接收事件的顺序。

在早期的浏览器大战中, Netscape(网景)和 IE 提出了两种完全相反的事件传播模型:

  • Netscape :主张事件捕获(Event Capturing),即事件从最外层向内层传播。
  • IE :主张事件冒泡(Event Bubbling),即事件从最内层向外层传播。

最终,W3C 制定了标准,将两者结合:先捕获,后冒泡

通俗比喻

想象你在一个多层办公楼里扔了一个石子(触发事件)。

  1. 捕获阶段 :石子从楼顶(window)一层层往下掉,经过每一层的地板。
  2. 目标阶段:石子落在了某一层的具体房间(目标元素)里。
  3. 冒泡阶段:石子落地后产生的回声或震动,从那个房间一层层往上传播回楼顶。

2. 🎬 事件流的三个阶段

当一个事件发生时,它会依次经历以下三个阶段:

第一阶段:捕获阶段(Capturing Phase)

  • 方向 :从根节点(window / document)向下传播,直到目标元素的父元素。
  • 特点:此时事件正在"寻找"目标。
  • 注意 :默认情况下,我们注册的事件监听器不会在这个阶段触发(除非特别指定)。

第二阶段:目标阶段(Target Phase)

  • 方向 :事件到达实际触发事件的元素(event.target)。
  • 特点:这是事件真正发生的地方。

第三阶段:冒泡阶段(Bubbling Phase)

  • 方向:从目标元素向上传播,经过父元素、祖父元素......直到根节点。
  • 特点:这是最常用的阶段。大多数事件监听器默认都注册在这个阶段。
text 复制代码
      Window
        |
      Document
        |
       HTML
        |
       Body          <--- 1. 捕获阶段 (向下)
        |            <--- 2. 目标阶段 (到达 div#target)
      Div#container  <--- 3. 冒泡阶段 (向上)
        |
      Div#target     (你点击了这里)

3. 💻 代码实战:如何控制捕获与冒泡?

在 JavaScript 中,我们使用 addEventListener 来注册事件。它的第三个参数决定了监听器在哪个阶段触发。

语法

javascript 复制代码
element.addEventListener(event, handler, useCapture);
  • useCapturetrue:在捕获阶段触发。
  • useCapturefalse(默认):在冒泡阶段触发。

🧪 实验代码

假设我们有如下 HTML 结构:

html 复制代码
<div id="grandparent">
  爷爷
  <div id="parent">
    爸爸
    <div id="child">儿子</div>
  </div>
</div>

我们给每一层都绑定点击事件,分别测试捕获和冒泡:

javascript 复制代码
const grandparent = document.getElementById("grandparent");
const parent = document.getElementById("parent");
const child = document.getElementById("child");

function log(msg, phase) {
  console.log(`${msg} - ${phase}`);
}

// 1. 注册捕获阶段监听器 (useCapture = true)
grandparent.addEventListener(
  "click",
  () => log("Grandparent", "Capture"),
  true,
);
parent.addEventListener("click", () => log("Parent", "Capture"), true);

// 2. 注册冒泡阶段监听器 (useCapture = false, 默认)
parent.addEventListener("click", () => log("Parent", "Bubble"));
child.addEventListener("click", () => log("Child", "Bubble"));

// 3. 点击 "儿子" (child)

📊 输出结果

当你点击 <div id="child"> 时,控制台输出顺序如下:

  1. Grandparent - Capture (捕获:从外向内)
  2. Parent - Capture (捕获:从外向内)
  3. Child - Bubble (目标:通常视为冒泡的一部分,或者单独的目标阶段)
  4. Parent - Bubble (冒泡:从内向外)

注意grandparent 没有注册冒泡监听器,所以最后没有输出 Grandparent - Bubble


4. 🛑 如何阻止事件传播?

有时候,我们不希望事件继续传播(例如:点击弹窗内部不希望关闭弹窗,但点击背景要关闭)。

方法一:event.stopPropagation()

作用:停止事件在 DOM 树中的进一步传播(既阻止后续的捕获,也阻止后续的冒泡)。

javascript 复制代码
child.addEventListener("click", function (event) {
  event.stopPropagation();
  console.log("Child clicked, stop!");
});

结果 :点击 child 后,只会执行 child 的回调,parentgrandparent 的回调都不会执行。

方法二:event.stopImmediatePropagation()

作用 :不仅阻止事件传播,还阻止当前元素上其他相同事件的监听器执行。

javascript 复制代码
child.addEventListener("click", function (event) {
  event.stopImmediatePropagation();
  console.log("Handler 1");
});

child.addEventListener("click", function () {
  console.log("Handler 2"); // 这行永远不会执行
});

❌ 误区:return false

在 jQuery 中,return false 等价于同时调用 stopPropagation()preventDefault()

但在原生 JS 中,return false 无效,必须显式调用上述方法。


5. 🚀 实际应用场景:为什么我们需要它们?

场景一:事件委托(利用冒泡)

这是冒泡最著名的应用。与其给 1000 个列表项分别绑定事件,不如给父元素 <ul> 绑定一个事件,利用冒泡机制统一处理。

javascript 复制代码
// 利用冒泡,只在 ul 上监听
ul.addEventListener("click", function (event) {
  if (event.target.tagName === "LI") {
    console.log("Clicked:", event.target.innerText);
  }
});

场景二:模态框关闭逻辑(混合使用)

常见需求:点击遮罩层关闭弹窗,点击弹窗内容不关闭。

html 复制代码
<div id="overlay">
  <div id="modal">内容...</div>
</div>
javascript 复制代码
const overlay = document.getElementById("overlay");
const modal = document.getElementById("modal");

// 1. 点击遮罩层,关闭弹窗(冒泡阶段)
overlay.addEventListener("click", () => {
  console.log("Close Modal");
});

// 2. 点击弹窗内部,阻止冒泡,防止触发 overlay 的点击事件
modal.addEventListener("click", (event) => {
  event.stopPropagation();
  console.log("Modal Content Clicked");
});

场景三:高级拦截(利用捕获)

如果你想在一个事件到达目标之前"拦截"它(例如全局权限检查、日志记录),可以使用捕获阶段。因为捕获发生在冒泡之前,你可以提前终止事件。

javascript 复制代码
// 在捕获阶段拦截所有点击
document.addEventListener(
  "click",
  (event) => {
    if (event.target.disabled) {
      event.stopPropagation(); // 禁止点击禁用元素
      event.preventDefault();
    }
  },
  true,
); // 注意这里是 true

💡 总结

特性 事件捕获 (Capturing) 事件冒泡 (Bubbling)
传播方向 从上到下 (Window -> Target) 从下到上 (Target -> Window)
触发时机 早期,用于拦截 晚期,用于处理
默认行为 需设置 useCapture: true 默认 (useCapture: false)
主要用途 事件拦截、全局监听 事件委托、常规交互

🚀 博主寄语

事件流就像水的流动,既有从天而降的雨(捕获),也有汇聚成河向上的蒸汽(冒泡)。

记住三个关键点

  1. 顺序:先捕获,后目标,再冒泡。
  2. 委托 :利用冒泡实现高性能的事件代理。
  3. 阻止 :使用 stopPropagation() 控制事件传播范围。

理解了事件流,你就掌握了 DOM 交互的脉搏!

希望这篇文档能帮你彻底搞懂事件捕获与冒泡!如果有疑问,欢迎在评论区留言。👇

喜欢这篇文章吗?记得点赞、收藏、转发哦! ❤️

相关推荐
小则又沐风a1 小时前
基础的开发工具(2)---Linux
java·linux·前端
一个学Java小白1 小时前
LV.12 Linux应用开发综合实战-在线词典
linux·运维·服务器
Csvn1 小时前
Vue 3 Composition API 深度解析
前端·vue.js
鹏程十八少1 小时前
11. 2026金三银四 能答对这 29 道题,你的 Android 插件化就算真正通关了
前端·后端·面试
开开心心_Every1 小时前
免费简洁的安卓黄历日历,软件推荐
运维·服务器·随机森林·pdf·电脑·excel·最小二乘法
林熙蕾LXL2 小时前
系统调用&文件描述
linux·运维·服务器
落羽的落羽2 小时前
【网络】TCP与UDP协议使用指南,Socket编程实现Echo服务
linux·服务器·网络·c++·网络协议·tcp/ip·机器学习
潇凝子潇2 小时前
使用英伟达免费调用多家大模型API
java·前端·javascript
旷世奇才李先生2 小时前
Vue 3\+Vite\+Pinia实战:前端工程化与组件化开发全指南
前端·vue.js