← [ BACK_TO_NOTES ]

前端事件传播

DOM 事件在捕获、目标和冒泡三个阶段中的传播过程,以及事件委托、传播控制与常见面试问法。

#frontend #concept #evergreen

[!info] related notes

前端事件传播

DOM 事件不是只在一个节点上孤立执行,它通常会沿着节点树传播。浏览器会让事件在多个相关节点之间按顺序流动,这就是事件传播机制。

一句话定义

一个 DOM 事件通常会经历捕获阶段、目标阶段和冒泡阶段,浏览器靠这套机制让目标元素与祖先元素都能参与事件处理。

为什么会有事件传播

如果你点击一个按钮,而按钮在 div 里,div 又在 body 里,浏览器至少要回答两件事:

  1. 真正触发事件的是谁
  2. 目标元素之外的父层要不要也有机会响应

所以浏览器设计了完整传播链:

  • 先从外往内找目标,这是捕获
  • 到达真正触发节点,这是目标阶段
  • 再从内往外通知祖先,这是冒泡

三个阶段

捕获阶段

事件从更外层祖先节点一层层向目标节点靠近。

典型顺序可以理解为:

window -> document -> html -> body -> ... -> target

如果监听器注册为捕获型,它会在这一阶段先执行。

parent.addEventListener('click', () => {
  console.log('parent capture')
}, true)

目标阶段

事件真正到达触发它的元素。

要注意两点:

  • 目标阶段不是“只执行一次”
  • 目标元素上既可能有捕获监听器,也可能有冒泡监听器

冒泡阶段

事件从目标节点开始,再一层层向外层祖先节点返回。

默认 addEventListener() 注册的是冒泡监听器,所以平时最常看到的是这一层。

parent.addEventListener('click', () => {
  console.log('parent bubble')
})

一个完整例子

<div id="outer">
  <div id="inner">
    <button id="btn">点击我</button>
  </div>
</div>
const outer = document.getElementById('outer')
const inner = document.getElementById('inner')
const btn = document.getElementById('btn')

outer.addEventListener('click', () => console.log('outer bubble'))
outer.addEventListener('click', () => console.log('outer capture'), true)

inner.addEventListener('click', () => console.log('inner bubble'))
inner.addEventListener('click', () => console.log('inner capture'), true)

btn.addEventListener('click', () => console.log('btn bubble'))
btn.addEventListener('click', () => console.log('btn capture'), true)

点击按钮时,常见输出顺序是:

outer capture
inner capture
btn capture
btn bubble
inner bubble
outer bubble

最短记忆:先一路向里,再一路向外。

事件对象里最重要的几个属性

event.target

真正触发事件的元素。

如果监听器绑在父元素 ul 上,但你点击的是子元素 li,那 event.target 仍然是 li。

event.currentTarget

当前正在执行监听器的元素。

ul.addEventListener('click', (event) => {
  console.log('target:', event.target)
  console.log('currentTarget:', event.currentTarget)
})

如果监听器绑在 ul 上:

  • target 是实际点击的元素
  • currentTarget 是当前处理监听器的 ul

event.eventPhase

表示当前事件处于哪个阶段。

  • 1:捕获阶段
  • 2:目标阶段
  • 3:冒泡阶段

addEventListener() 和传播阶段的关系

element.addEventListener('click', handler)
element.addEventListener('click', handler, true)
element.addEventListener('click', handler, { capture: true })
  • 默认不传第三个参数时,监听器注册在冒泡阶段
  • 传 true 或 { capture: true } 时,注册在捕获阶段

如果要系统理解这个 API,再看 add-event-listener。

哪些事件会冒泡,哪些默认不会

常见会冒泡的事件

  • click
  • mousedown
  • mouseup
  • keydown
  • keyup
  • input
  • change

常见默认不冒泡的事件

  • focus
  • blur
  • mouseenter
  • mouseleave

但要知道一些替代事件会冒泡:

  • focusin / focusout
  • mouseover / mouseout

事件委托为什么能成立

因为子元素触发的事件会冒泡到父元素,所以父元素可以统一监听,再根据事件来源判断到底是哪个子元素触发了动作。

<ul id="list">
  <li data-id="1">A</li>
  <li data-id="2">B</li>
  <li data-id="3">C</li>
</ul>
const list = document.getElementById('list')

list.addEventListener('click', (event) => {
  const li = event.target.closest('li')
  if (!li) return

  console.log('点击了', li.dataset.id)
})

事件委托的典型价值

  • 减少大量子节点重复绑定监听器
  • 动态新增子节点时,不必重新逐个绑定
  • 更适合列表、菜单、表格等场景
  • 逻辑集中,更容易维护

冒泡和事件委托不是一回事

  • 冒泡是浏览器的传播机制
  • 事件委托是开发者基于冒泡实现的编码策略

控制传播时最容易混淆的三个 API

stopPropagation()

阻止事件继续向父级传播,但不会阻止当前元素上的其他同类型监听器执行。

stopImmediatePropagation()

更强。它既阻止继续传播,也阻止当前元素后续同类型监听器继续执行。

preventDefault()

阻止默认行为,不是阻止传播。

例如:

  • 点击 <a> 不跳转
  • 提交表单不刷新

最短区分:

  • preventDefault() 管默认行为
  • stopPropagation() 管传播路径

实际开发中的常见场景

列表点击高亮

把监听绑在父容器上,通过 target 或 closest() 找到具体项。

表格行操作

整张表统一监听,减少大量按钮绑定。

动态内容区域

弹窗、评论列表、异步渲染内容,不需要每次重新逐个绑定事件。

页面级关闭逻辑

比如点击弹窗外区域关闭弹窗,常依赖传播链和目标判断完成。

常见误区

  • 以为事件只在目标节点执行一次
  • 把冒泡和事件委托当成同一个概念
  • 认为所有事件都会冒泡
  • 把 preventDefault() 和 stopPropagation() 混为一谈
  • 误以为 event.target 就是绑定监听器的元素
  • 一看到联动异常就盲目 stopPropagation(),导致外层交互能力被意外切断

与 React 合成事件的边界

本页维护原生 DOM Event propagation。

React 的 Synthetic Event、事件处理器与 state update 的关系统一看 React 事件处理与状态更新;capture、once、passive、signal 等监听器注册选项统一看 addEventListener。

学习检查

不看正文回答:

  1. DOM 事件的 capture → target → bubble 三个阶段分别沿什么方向传播?
  2. event.target 与 event.currentTarget 分别表示谁?
  3. 事件委托为什么建立在冒泡机制上,却不等于冒泡本身?
  4. preventDefault()、stopPropagation()、stopImmediatePropagation() 分别阻止什么?
  5. 为什么动态新增的子元素通常也能被父元素委托监听器处理?
  6. 为什么委托时 target.closest(selector) 往往比直接判断 target.tagName 更稳?
  7. 哪些事件默认不冒泡,为什么这会影响委托方案?
  8. 给一个三层 DOM,分别注册 capture/bubble listener,闭卷写出点击最内层节点时的传播顺序。
创建于 2026/3/19 更新于 2026/9/30
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本