《搞懂事件机制,只要这一篇:JS 事件捕获/冒泡全流程讲解》

154 阅读3分钟

JavaScript 事件机制:从捕获到冒泡,一次讲清楚

前端世界中最神秘、出现频率最高、同时也是最容易让初学者困惑的东西之一,就是「事件」。
你点击一个按钮、你把鼠标移到一个元素上、你按下键盘按键——这些动作都会触发事件。

如果把网页比作一个舞台,DOM 节点就是演员;而事件,就是观众对演员做出的“呼喊”、舞台灯光的“变化”、甚至幕布的“升起下降”。JavaScript 作为导演,需要知道:
是谁触发了事件?事件经过了哪些演员?它什么时候被捕捉、什么时候传递?又是谁最终做出回应?

本文将从最底层的原理讲起,再结合代码示例,通过通俗类比讲清楚 JavaScript 的事件机制。接下来让我们开始这趟旅程。

一、事件是怎么发生的?——从 DOM 树说起

网页并不是“平面画”,而是一棵树(DOM Tree)。例如结构:

document
└── html
    └── body
        └── div (#parent)
            └── div (#child)

当你点击 child,事件沿着这棵树会经历 三个阶段:捕获 → 目标 → 冒泡

这就是浏览器的“事件传播路径”。


二、事件传播

下面这张图是事件机制最清晰的呈现方式 —— 解释事件传播顺序之前

image.png

图中的流程可概括为:

  1. window → document → html → body → div(捕获阶段)
  2. div(目标阶段)
  3. div → body → html → document → window(冒泡阶段)

记住:事件一定会 先捕获→再冒泡(除非被阻止)。


三、用生活比喻理解三阶段

把整个网页比作一栋房子:

  • window 是屋顶
  • document 是门厅
  • html 是大厅
  • body 是走廊
  • div 是房间
  • child 是房间里的桌子

你点击 child,相当于“桌子发出了声音”。

1. 捕获阶段:从外往里听

屋顶 → 门厅 → 大厅 → 走廊 → 房间

2. 目标阶段:找到声音来源

桌子本身处理事件

3. 冒泡阶段:从里往外通知

房间 → 走廊 → 大厅 → 门厅 → 屋顶


四、addEventListener 与 useCapture

事件监听主要通过:

element.addEventListener(type, handler, useCapture)
  • useCapture = false(默认):冒泡阶段触发
  • useCapture = true:捕获阶段触发

例如:

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

document.getElementById('child').addEventListener('click', (event) => {
  event.stopPropagation();
  console.log('child');
}, false);

因为 child 阻止了冒泡,因此只有输出:

child

五、stopPropagation:中断传播

event.stopPropagation() 会让事件停止向外传播,就像孩子对父亲说:

“我已经处理好了,不必继续传递!”

这在 child 的代码里已经展示得非常典型。


六、event.target 与 currentTarget

这是事件里最容易混淆的部分:

  • event.target:真正触发事件的元素(谁被点了)
  • event.currentTarget:当前执行监听器的元素(谁在处理)

举例:

parent.addEventListener('click', function (event) {
  console.log(event.target);        // child
  console.log(event.currentTarget); // parent
});

因为你点击 child,但监听器挂在 parent。


七、为什么事件监听不能加在集合上?

例如:

const lis = document.querySelectorAll('#list li');
// lis.addEventListener(...) ❌

因为 NodeList 不是 DOM 节点,没有 addEventListener。

可以这么写:

lis.forEach(li => li.addEventListener(...))

但这样监听器会很多,性能差。


八、推荐方案:事件委托(Event Delegation)

document.getElementById('list').addEventListener('click', function(event) {
  console.log(event.target.innerHTML);
});

这是最佳实践。

原因:

1. li 的点击事件会冒泡到 ul

2. 我们只需监听一次

3. 新增 li 时无需重新绑定事件**

好处:

1. 性能更佳

2. 结构更清晰

3. 动态元素自动支持事件


完整流程小结

  • 网页是 DOM 树,事件沿着树传播
  • 顺序永远是:
    捕获 → 目标 → 冒泡

一次点击 child 的真实执行顺序:

  1. 捕获阶段
    window → document → html → body → parent → child
  2. 目标阶段
    child 的事件触发
  3. 冒泡阶段
    child → parent → body → html → document → window
    (如果未被 stopPropagation)

这是事件机制的全部核心


结语

JavaScript 的事件机制是前端交互的基石。理解事件流的三个阶段、掌握 addEventListener 的使用、善用事件委托,不仅能写出更高效的代码,还能避免常见的性能与内存问题。记住:事件是异步的、流式的、可委托的。掌握这些核心思想,你就能在复杂的用户交互场景中游刃有余。