人人都会AI编程

15.1 事件流三阶段:捕获阶段、目标阶段、冒泡阶段

更新时间:2026-07-11

当你在浏览器中点击一个按钮时,这个“点击”事件并非直接在按钮上触发一下就结束了。实际上,事件经历了一条从外到内、再从内到外的完整传播路径,这条路径被称为事件流。W3C 标准规定,一个完整的 DOM 事件流包含三个依次进行的阶段:捕获阶段目标阶段冒泡阶段。理解这三个阶段,是掌握事件机制、事件委托和事件优化技巧的基石。

15.1.1 三个阶段的完整过程

用一个简单的页面结构来直观展示三个阶段:

<html>
  <body>
    <div id="outer">
      <button id="btn">点击我</button>
    </div>
  </body>
</html>

当用户点击 <button> 按钮时,事件流按照以下顺序传播:

  1. 捕获阶段(Capturing Phase)

事件从最外层的 window 对象开始,沿着 DOM 树向下传递,经过 documenthtmlbodydiv#outer,一直到达事件的目标元素 button#btn父节点。在这个阶段,位于路径上的祖先元素可以通过事件监听器“提前拦截”到该事件。

  1. 目标阶段(Target Phase)

事件到达真正的目标元素(button#btn)自身。在这一阶段,目标元素上注册的监听器会触发。需要注意的是,即使监听器被设置为“捕获阶段触发”,在目标元素上也会触发(目标元素上的捕获和冒泡监听都会被执行,触发顺序取决于注册顺序)。

  1. 冒泡阶段(Bubbling Phase)

事件从目标元素开始,沿 DOM 树向上回溯,经过 div#outerbodyhtmldocument,最终回到 window。在这个阶段,祖先元素可以监听到发生在子元素上的事件。大部分常用事件(如 click、mousedown、keydown 等)都会冒泡,但并非所有事件都支持冒泡(例如 focusblurloadunloadscroll 等不冒泡)。

15.1.2 如何在特定阶段监听事件

addEventListener 方法的第三个参数决定了监听器在哪个阶段触发:

// 语法
element.addEventListener(eventType, listener, useCapture);
  • useCapturetrue(或 {capture: true})时,监听器在捕获阶段触发。
  • useCapturefalse(默认值)时,监听器在冒泡阶段触发。

代码验证

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 节。
  • 事件顺序控制:当多个元素嵌套且都监听了同一事件类型时,利用捕获与冒泡的先后顺序,可以精确控制处理函数的执行次序。
  • 避免“点击穿透”:在移动端,有些情况下需要阻止事件冒泡以避免意外的上层元素行为触发,比如遮罩层点击关闭时,需要阻止下层元素的点击事件被意外触发。
  • 调试事件流:当点击一个元素却触发了意料之外的元素行为时,追踪事件流可以快速定位是哪一个祖先元素“拦截”或“响应”了事件。

事件流三阶段是浏览器事件系统的骨架。下一节将深入讲解如何绑定与移除事件,并展示不同绑定方式的差异,这些知识都建立在三阶段模型之上。