JavaScript 事件机制:从捕获到冒泡,一次讲清楚
前端世界中最神秘、出现频率最高、同时也是最容易让初学者困惑的东西之一,就是「事件」。
你点击一个按钮、你把鼠标移到一个元素上、你按下键盘按键——这些动作都会触发事件。
如果把网页比作一个舞台,DOM 节点就是演员;而事件,就是观众对演员做出的“呼喊”、舞台灯光的“变化”、甚至幕布的“升起下降”。JavaScript 作为导演,需要知道:
是谁触发了事件?事件经过了哪些演员?它什么时候被捕捉、什么时候传递?又是谁最终做出回应?
本文将从最底层的原理讲起,再结合代码示例,通过通俗类比讲清楚 JavaScript 的事件机制。接下来让我们开始这趟旅程。
一、事件是怎么发生的?——从 DOM 树说起
网页并不是“平面画”,而是一棵树(DOM Tree)。例如结构:
document
└── html
└── body
└── div (#parent)
└── div (#child)
当你点击 child,事件沿着这棵树会经历 三个阶段:捕获 → 目标 → 冒泡。
这就是浏览器的“事件传播路径”。
二、事件传播
下面这张图是事件机制最清晰的呈现方式 —— 解释事件传播顺序之前。
图中的流程可概括为:
- window → document → html → body → div(捕获阶段)
- div(目标阶段)
- 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 的真实执行顺序:
- 捕获阶段
window → document → html → body → parent → child - 目标阶段
child 的事件触发 - 冒泡阶段
child → parent → body → html → document → window
(如果未被 stopPropagation)
这是事件机制的全部核心。
结语
JavaScript 的事件机制是前端交互的基石。理解事件流的三个阶段、掌握 addEventListener 的使用、善用事件委托,不仅能写出更高效的代码,还能避免常见的性能与内存问题。记住:事件是异步的、流式的、可委托的。掌握这些核心思想,你就能在复杂的用户交互场景中游刃有余。