当你在浏览器中点击一个按钮时,这个“点击”事件并非直接在按钮上触发一下就结束了。实际上,事件经历了一条从外到内、再从内到外的完整传播路径,这条路径被称为事件流。W3C 标准规定,一个完整的 DOM 事件流包含三个依次进行的阶段:捕获阶段、目标阶段和冒泡阶段。理解这三个阶段,是掌握事件机制、事件委托和事件优化技巧的基石。
15.1.1 三个阶段的完整过程
用一个简单的页面结构来直观展示三个阶段:
<html>
<body>
<div id="outer">
<button id="btn">点击我</button>
</div>
</body>
</html>
当用户点击 <button> 按钮时,事件流按照以下顺序传播:
- 捕获阶段(Capturing Phase)
事件从最外层的 window 对象开始,沿着 DOM 树向下传递,经过 document、html、body、div#outer,一直到达事件的目标元素 button#btn 的父节点。在这个阶段,位于路径上的祖先元素可以通过事件监听器“提前拦截”到该事件。
- 目标阶段(Target Phase)
事件到达真正的目标元素(button#btn)自身。在这一阶段,目标元素上注册的监听器会触发。需要注意的是,即使监听器被设置为“捕获阶段触发”,在目标元素上也会触发(目标元素上的捕获和冒泡监听都会被执行,触发顺序取决于注册顺序)。
- 冒泡阶段(Bubbling Phase)
事件从目标元素开始,沿 DOM 树向上回溯,经过 div#outer、body、html、document,最终回到 window。在这个阶段,祖先元素可以监听到发生在子元素上的事件。大部分常用事件(如 click、mousedown、keydown 等)都会冒泡,但并非所有事件都支持冒泡(例如 focus、blur、load、unload、scroll 等不冒泡)。
15.1.2 如何在特定阶段监听事件
addEventListener 方法的第三个参数决定了监听器在哪个阶段触发:
// 语法
element.addEventListener(eventType, listener, useCapture);
- 当
useCapture为true(或{capture: true})时,监听器在捕获阶段触发。 - 当
useCapture为false(默认值)时,监听器在冒泡阶段触发。
代码验证:
const outer = document.getElementById('outer');
const btn = document.getElementById('btn');
// 捕获阶段监听(祖先元素)
outer.addEventListener('click', () => {
console.log('outer 捕获阶段');
}, true);
// 冒泡阶段监听(祖先元素)
outer.addEventListener('click', () => {
console.log('outer 冒泡阶段');
}, false);
// 目标元素(默认冒泡)
btn.addEventListener('click', () => {
console.log('btn 目标阶段');
});
点击按钮后,控制台输出顺序为:
outer 捕获阶段
btn 目标阶段
outer 冒泡阶段
这个顺序清晰地展示了捕获 → 目标 → 冒泡的流程。
15.1.3 event.eventPhase 属性
事件对象提供 eventPhase 属性,用来指示当前所处的阶段:
1:捕获阶段2:目标阶段3:冒泡阶段
可以配合事件监听器精确判断事件阶段,尽管实际开发中较少使用,但在调试事件流时非常实用。
15.1.4 控制事件传播:停止与阻止
在事件流中,你可以在任意阶段终止事件的继续传播,或者阻止浏览器的默认行为。
- 阻止事件传播:
event.stopPropagation()
调用该方法后,事件将不再继续向下(捕获阶段)或向上(冒泡阶段)传播,但不会影响当前节点上其他尚未触发的监听器。如果需要阻止同一节点上的其他监听器,应使用 event.stopImmediatePropagation()。
- 阻止默认行为:
event.preventDefault()
阻止浏览器对当前事件的默认处理,例如阻止链接跳转、阻止表单提交、阻止按键输入字符等。注意,该方法不会阻止事件传播。
15.1.5 哪些事件不支持冒泡
并非所有事件都会经历完整的捕获-目标-冒泡三阶段。以下常见事件不冒泡:
focus/blur(但对应的focusin/focusout支持冒泡)load/unload/error/scroll(部分浏览器对scroll的处理有差异)mouseenter/mouseleave(它们不会冒泡,但mouseover/mouseout会冒泡)
对于不冒泡的事件,祖先元素无法通过冒泡阶段监听到,只能通过捕获阶段或直接在目标元素上监听。使用事件委托时必须注意这一点。
15.1.6 实际开发意义
理解事件流的三个阶段,能帮助你解决日常开发中的几类典型问题:
- 事件委托(事件代理):利用冒泡机制,在祖先元素上统一监听动态添加的子元素事件,减少监听器数量,优化性能。详见 15.4 节。
- 事件顺序控制:当多个元素嵌套且都监听了同一事件类型时,利用捕获与冒泡的先后顺序,可以精确控制处理函数的执行次序。
- 避免“点击穿透”:在移动端,有些情况下需要阻止事件冒泡以避免意外的上层元素行为触发,比如遮罩层点击关闭时,需要阻止下层元素的点击事件被意外触发。
- 调试事件流:当点击一个元素却触发了意料之外的元素行为时,追踪事件流可以快速定位是哪一个祖先元素“拦截”或“响应”了事件。
事件流三阶段是浏览器事件系统的骨架。下一节将深入讲解如何绑定与移除事件,并展示不同绑定方式的差异,这些知识都建立在三阶段模型之上。