前端事件传播
DOM 事件在捕获、目标和冒泡三个阶段中的传播过程,以及事件委托、传播控制与常见面试问法。
[!info] related notes
- 所属 MOC: 前端基础 MOC, 浏览器 JS MOC
- 上位主题: 浏览器中的 JavaScript
- 相关概念: javascript-events, frontend-event-bubbling, add-event-listener, debounce-and-throttle
- React 事件: React 事件处理与状态更新
- 监听器选项: addEventListener
前端事件传播
DOM 事件不是只在一个节点上孤立执行,它通常会沿着节点树传播。浏览器会让事件在多个相关节点之间按顺序流动,这就是事件传播机制。
一句话定义
一个 DOM 事件通常会经历捕获阶段、目标阶段和冒泡阶段,浏览器靠这套机制让目标元素与祖先元素都能参与事件处理。
为什么会有事件传播
如果你点击一个按钮,而按钮在 div 里,div 又在 body 里,浏览器至少要回答两件事:
- 真正触发事件的是谁
- 目标元素之外的父层要不要也有机会响应
所以浏览器设计了完整传播链:
- 先从外往内找目标,这是捕获
- 到达真正触发节点,这是目标阶段
- 再从内往外通知祖先,这是冒泡
三个阶段
捕获阶段
事件从更外层祖先节点一层层向目标节点靠近。
典型顺序可以理解为:
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。
哪些事件会冒泡,哪些默认不会
常见会冒泡的事件
clickmousedownmouseupkeydownkeyupinputchange
常见默认不冒泡的事件
focusblurmouseentermouseleave
但要知道一些替代事件会冒泡:
focusin/focusoutmouseover/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。
学习检查
不看正文回答:
- DOM 事件的 capture → target → bubble 三个阶段分别沿什么方向传播?
event.target与event.currentTarget分别表示谁?- 事件委托为什么建立在冒泡机制上,却不等于冒泡本身?
preventDefault()、stopPropagation()、stopImmediatePropagation()分别阻止什么?- 为什么动态新增的子元素通常也能被父元素委托监听器处理?
- 为什么委托时
target.closest(selector)往往比直接判断target.tagName更稳? - 哪些事件默认不冒泡,为什么这会影响委托方案?
- 给一个三层 DOM,分别注册 capture/bubble listener,闭卷写出点击最内层节点时的传播顺序。