前言
JavaScript 的事件机制是前端开发和面试中最核心、最容易被问到的知识点之一。几乎所有大厂前端面试都会涉及“事件冒泡”“捕获”“阻止冒泡”“事件委托”等话题。本文从零开始,由浅入深,结合实际代码和常见面试题,帮你彻底搞懂 JS 事件流。
一、事件到底是怎么发生的?
浏览器渲染页面时,会把 HTML 构建成一棵 DOM 树。当用户与页面交互(点击、输入、滚动等)时,浏览器就会触发一个“事件”。
这个事件不是直接只作用在你点的那一个元素上,而是会沿着 DOM 树“走”一遍,这个“走”的过程分为三个阶段:
捕获阶段(Capture) → 目标阶段(Target) → 冒泡阶段(Bubbling)
- 捕获阶段(从外到内) 从 window → document → → → … → 一路向下找到真正被点击的元素(目标元素)。 这个阶段默认不执行监听器,除非你显式设置 useCapture: true。
- 目标阶段 事件到达真正触发事件的元素(event.target),在此阶段注册的监听器会执行(无论捕获还是冒泡)。
- 冒泡阶段(从内到外) 事件从目标元素开始,一层一层向外“冒泡”,重新回到 window。 这是我们最常用的阶段,addEventListener 第三个参数默认就是 false(冒泡阶段) 。
二、addEventListener 的第三个参数到底是干嘛的?
element.addEventListener(eventType, handler, useCapture)
- useCapture = false(默认):在冒泡阶段执行(99% 的情况我们都用这个)
- useCapture = true:在捕获阶段执行
<div id="parent">
<div id="child">点我</div>
</div>
parent.addEventListener('click', () => console.log('parent 捕获'), true);
parent.addEventListener('click', () => console.log('parent 冒泡'), false);
child.addEventListener('click', () => console.log('child 冒泡'), false);
child.addEventListener('click', () => console.log('child 捕获'), true);
点击 child,执行顺序是:
- parent 捕获
- child 捕获
- child 冒泡 ← 目标阶段同时属于捕获和冒泡
- parent 冒泡
面试题:请写出点击 child 时的完整执行顺序? 这就是最经典的事件流顺序题。
三、如何阻止冒泡?—— stopPropagation() 与 stopImmediatePropagation()
child.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止继续向上冒泡
console.log('child');
});
加上这句后,parent 上的所有监听器(无论捕获还是冒泡)都不会再触发。
stopImmediatePropagation() 更狠:不仅阻止冒泡,还阻止当前元素上同类型其他监听器继续执行。
child.addEventListener('click', () => console.log(1));
child.addEventListener('click', (e) => {
console.log(2);
e.stopImmediatePropagation();
});
child.addEventListener('click', () => console.log(3)); // 不会执行
// 输出:1 2
面试题:stopPropagation 和 stopImmediatePropagation 的区别?
四、为什么我们要关心冒泡?—— 事件委托(Event Delegation)
传统做法(不推荐):
document.querySelectorAll('li').forEach(li => {
li.addEventListener('click', handler);
});
缺点:
- 性能差:li 成千上万个就要绑定成千上万次
- 动态添加的 li 不生效
事件委托利用了冒泡机制,只绑定一次:
<ul id="list">
<li>1</li>
<li>2</li>
<li>3</li>
<!-- 动态添加的也会生效 -->
</ul>
document.getElementById('list').addEventListener('click', function(e) {
if (e.target.tagName === 'LI') {
console.log(e.target.innerHTML);
}
});
优点:
- 性能极高,只绑定一次
- 动态生成的子元素自动支持
- 内存占用极低
面试必考:请手写一个事件委托,实现点击不同 li 高亮选中状态(单选/多选都可能问)。
let selected = null;
list.addEventListener('click', function(e) {
if (e.target.tagName === 'LI') {
if (selected) selected.style.background = '';
e.target.style.background = 'yellow';
selected = e.target;
}
});
五、body 和 document 上的默认冒泡行为(常被忽略)
<body onclick="alert('body')">
<div id="child">点我</div>
</body>
即使你没在 child 上阻止冒泡,点击 child 也会弹出 alert,因为事件会一直冒泡到 body!
很多新手以为只点了 child,实际上 body、document、window 都会收到事件。
六、面试高频总结(直接背)
| 题目 | 标准答案要点 |
|---|---|
| 事件执行顺序 | 父捕获 → 子捕获 → 子冒泡(目标阶段) → 父冒泡 |
| addEventListener 第三个参数作用 | true=捕获阶段,false=冒泡阶段(默认) |
| 阻止冒泡 | e.stopPropagation() |
| 阻止冒泡且阻止当前元素后续同类型监听器 | e.stopImmediatePropagation() |
| 事件委托原理 | 利用冒泡 + e.target 判断真正点击的元素 |
| 事件委托优点 | 性能好、动态元素自动支持、内存占用低 |
| 为什么不推荐 DOM0 级事件(element.onclick=) | 只能绑定一个、不好移除、不支持捕获阶段 |
| event.target 和 event.currentTarget 区别 | target 是真正触发事件的元素,currentTarget 是绑定事件的元素(委托时常用) |
七、完整示例代码(建议直接复制运行)
<!DOCTYPE html>HTML
<html>
<head>
<style>
.box { padding: 50px; background: #ccc; margin: 20px; }
#grandpa { background: #aaa; }
#parent { background: #f90; }
#child { background: #f00; width: 100px; height: 100px; }
</style>
</head>
<body>
<div id="grandpa" class="box">
grandpa
<div id="parent" class="box">
parent
<div id="child" class="box">child</div>
</div>
</div>
<script>
const log = (pos, phase) => console.log(`${pos} ${phase}`);
grandpa.addEventListener('click', () => log('grandpa', '捕获'), true);
grandpa.addEventListener('click', () => log('grandpa', '冒泡'), false);
parent.addEventListener('click', () => log('parent', '捕获'), true);
parent.addEventListener('click', () => log('parent', '冒泡'), false);
child.addEventListener('click', (e) => {
log('child', '捕获/冒泡');
// e.stopPropagation(); // 打开这句试试效果
}, true);
child.addEventListener('click', () => log('child', '重复绑定测试'), false);
</script>
</body>
</html>
点击红色 child 区域,控制台输出顺序就是最标准的事件流答案。
掌握了上面所有内容,JS 事件相关面试题基本再也难不倒你。祝你面试顺利,拿下大厂 offer!