先记住这个答案
通常把监听器注册在冒泡阶段更符合常见的事件委托和组件处理。若父容器必须在子按钮处理之前观察事件,可传 { capture: true },让回调在事件沿祖先链向目标前进时执行。捕获也能用于监听某些不冒泡事件,例如祖先上捕获子输入框的 focus。但它不是绕过所有传播控制的特权通道。
- 捕获沿祖先到目标传播
capture: true让父层先接收- 可用于观察部分不冒泡事件
- 不能穿过更早的传播阻断
表单容器先观察焦点
表单想统计用户首次进入哪个输入框,普通 focus 不冒泡,直接在表单上加默认监听不会像 click 那样收到子字段的焦点事件。捕获监听可沿传播路径观察到它。
const form = document.querySelector('#profile')
form.addEventListener('focus', event => {
const field = event.target
console.log('聚焦字段:', field.name) // 焦点到达字段前经过表单
}, { capture: true })这里读取的是实际触发焦点的字段。若业务只需要会冒泡的焦点进入事件,也可改用 focusin,不必依赖捕获。
捕获监听运行较早,适合观察或少量前置控制;若在这里随意调用 stopPropagation(),可能让子组件监听不到它期待的事件。
捕获不是万能保险
事件从更外层开始进入。如果外层捕获监听先停止传播,内层捕获监听也不会得到这次事件。闭合 Shadow DOM 等封装边界还可能改变可见目标和路径。
排查“为什么没有触发”时,先画出目标与祖先链,检查事件是否经过该节点、是否冒泡或跨边界,再看哪个监听器在何阶段阻断传播。不要只把 capture 改成 true 试运气。
同一个元素上同时注册捕获和非捕获监听,事件到达目标时会先处理捕获监听,再处理普通监听;同阶段的多个监听按注册顺序执行。若业务要求一个操作严格先于另一个,优先在自己的处理流程里明确调用次序,少靠分散监听器之间的隐含约定。
移除捕获监听时还必须把捕获标志传对:注册用 { capture: true },清理却省略第三个参数,浏览器不会把它当作同一条监听器。组件反复挂载后若出现同一事件被记录多次,可以先检查这里。
容易答错的地方
- 认为捕获能监听任何页面事件
- 目标与监听元素之间必须有传播路径关系,且可能被更早的监听器阻断。
- 把捕获等同于取消默认行为
- 捕获只决定回调运行阶段;是否取消默认行为还要看事件是否可取消并调用
preventDefault()。
面试官还会怎么问?
focus 不冒泡,怎样做表单级监听?
可以在表单祖先捕获 focus,也可使用会冒泡的 focusin。
捕获和冒泡都注册同一事件会怎样?
两个监听器在各自阶段执行,调用顺序取决于传播路径与目标位置。
子元素 stopPropagation() 能挡住祖先捕获吗?
子元素回调发生前,祖先捕获监听通常已经运行;它不能回头撤销已执行的回调。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。