JS 事件机制:从 DOM 树到事件冒泡,一文搞懂核心原理

44 阅读3分钟

今天想和大家深入聊聊 JavaScript 中一个非常基础但又极其重要的知识点——事件机制(Event Mechanism) 。你是否曾困惑过为什么点击子元素时父元素也会触发?或者为什么有时候事件监听器没有执行?这篇文章将结合图示与代码,带你彻底理解 JS 的事件流程。


🌐 什么是事件?

在 Web 开发中,用户行为(如点击、输入、滚动等)会触发“事件”。JavaScript 通过事件机制来响应这些行为。它不仅是交互的核心,更是现代前端框架(如 React、Vue)的基础之一。

✅ 事件的本质

  • 异步执行:事件不会阻塞主线程。
  • 基于 DOM 树结构传播:事件在 DOM 节点之间传递。
  • 可被控制:我们可以通过 stopPropagation() 等方法阻止其传播。

🔍 事件的三个阶段:捕获 → 目标 → 冒泡

图1:DOM 树结构与事件传播路径

(注:此处为示意描述,请替换为你实际提供的图)

当我们在页面上点击某个元素时,事件并不会只在该元素上执行一次。而是按照以下三阶段进行:

1️⃣ 捕获阶段(Capture Phase)

  • windowdocumenthtmlbody → …… → 最终到达目标元素。
  • 这个过程是“向下”的,逐层接近目标。
  • 默认情况下,大多数事件监听器不在此阶段执行(除非设置了 useCapture: true)。

2️⃣ 目标阶段(Target Phase)

  • 到达真正被点击的元素(即 event.target)。
  • 所有绑定在这个元素上的事件都会在这里执行。

3️⃣ 冒泡阶段(Bubbling Phase)

  • 从目标元素开始向上回溯,直到 window
  • 大多数事件默认都是冒泡的,比如 clickmouseover 等。

📌 关键点

addEventListener('click', callback, useCapture)
  • useCapture = false:在冒泡阶段执行(默认)。
  • useCapture = true:在捕获阶段执行。

💡 实战演示:父子元素点击事件

图2:HTML + CSS + JS 示例代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>JS 事件机制</title>
  <style>
    #parent {
      width: 200px;
      height: 200px;
      background-color: red;
    }
    #child {
      width: 100px;
      height: 100px;
      background-color: blue;
    }
  </style>
</head>
<body onload="alert('机制')">
  <div id="parent">
    <div id="child"></div>
  </div>

  <script>
    document.getElementById('parent').addEventListener('click', function() {
      console.log('parent click');
    }, false);

    document.getElementById('child').addEventListener('click', function(event) {
      event.stopPropagation(); // 阻止冒泡
      console.log('child click');
    }, false);
  </script>
</body>
</html>

🧪 运行结果分析:

  1. 点击蓝色 child 区域:

    • 控制台输出:child click
    • 不会输出 parent click,因为调用了 stopPropagation()
  2. 如果去掉 stopPropagation()

    • 输出顺序为:child clickparent click
    • 因为事件先在目标阶段执行,然后冒泡到父级。

✅ 小结:

  • 使用 event.stopPropagation() 可以防止事件继续向上传播。
  • 事件监听必须绑定在单个 DOM 元素上,不能直接作用于集合(如 NodeList)。

🚀 高级技巧:事件委托(Event Delegation)

图3:使用事件委托处理多个 li 元素

<ul id="list">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

<script>
  const lis = document.querySelectorAll('#list li');
  // for (let i = 0; i < lis.length; i++) {
  //   lis[i].addEventListener('click', function() {
  //     console.log(this.innerHTML);
  //   });
  // }

  document.getElementById('list').addEventListener('click', function(event) {
    console.log('------------------');
    console.log(event.target.innerHTML); // 获取实际点击的元素内容
  });
</script>

✨ 优点:

方案缺点优点
为每个 <li> 添加监听性能差,内存占用大易维护,动态添加节点也能生效

👉 推荐做法:将事件绑定在父容器上,利用 event.target 获取真实触发元素。

⚠️ 注意:event.target 是实际触发事件的元素,而 this 是当前监听器绑定的元素。


📌 常见误区与最佳实践

问题解决方案
事件监听器无法在集合上绑定使用 querySelectorAll 获取后遍历或采用事件委托
内存泄漏风险及时移除不再需要的监听器:removeEventListener()
误以为事件同步执行记住:事件是异步的,可能影响后续逻辑执行顺序
忽略事件冒泡导致重复执行使用 stopPropagation()preventDefault() 控制行为

🧩 总结:JS 事件机制核心要点

  1. 事件是异步的:注册后不会立即执行。
  2. 事件流分为三阶段:捕获 → 目标 → 冒泡。
  3. addEventListener() 是标准方式:优于内联事件(如 onclick)。
  4. event.target 表示触发元素this 表示绑定元素。
  5. 事件委托节省性能:尤其适合动态列表项。
  6. 避免滥用事件监听:注意内存开销,及时清理。