⭐ 推荐:社区规则条款 V1.0

js 事件冒泡和事件捕获是什么

chinaz
chinaz 初级会员萌新站长
发布于 2026-10-17 10:31 ·3 浏览 ·0 回复
内容摘要

事件捕获与冒泡是同一事件传播中方向相反的两个阶段,捕获先于冒泡;`addEventListener` 第三个参数决定监听阶段,目标元素上的监听按注册顺序执行,事件委托依赖冒泡。

JavaScript 事件在 DOM 树中按「捕获 → 目标 → 冒泡」三个阶段传播:事件捕获是从 window 向下传到目标元素的阶段,事件冒泡是从目标元素向上传回 window 的阶段。绑定监听时决定走哪个阶段的是 addEventListener(type, listener, options) 的第三个参数——传 true(或 {capture: true})就在捕获阶段触发,省略或传 false 就在冒泡阶段触发。日常的事件委托依赖冒泡实现,需要在事件到达目标前拦截时才用捕获。

事件冒泡和事件捕获分别是什么?

结论:两者是同一个事件传播过程的两个方向相反的阶段,捕获先发生,冒泡后发生。

捕获阶段(capture phase)从最外层 window 开始,逐层向下传播,直到目标元素的父元素;冒泡阶段(bubble phase)从目标元素逐层向上,一直传到 document 和 window。1990 年代 Netscape 主张捕获、IE 主张冒泡,DOM Level 2 的 addEventListener 把两套模型合并,形成了今天统一的三阶段事件流。

目标元素本身所在的阶段叫目标阶段(target phase),此时 event.eventPhase === 2;捕获阶段是 1,冒泡阶段是 3。可以在监听器里打印 event.eventPhase 验证自己处在哪一段:

document.addEventListener('click', e => console.log(e.eventPhase), true); // 1

一次点击的完整触发顺序是什么?

结论:同一个点击事件,完整顺序是「document 捕获 → 外层捕获 → 目标 → 外层冒泡 → document 冒泡」。

用下面这段代码可以直接跑出结果:

<div class="outer"><button class="inner">点我</button></div>
const outer = document.querySelector('.outer');
const inner = document.querySelector('.inner');

document.addEventListener('click', () => console.log('1 document 捕获'), true);
outer.addEventListener('click', () => console.log('2 outer 捕获'), true);
inner.addEventListener('click', () => console.log('3 inner 目标'));
outer.addEventListener('click', () => console.log('4 outer 冒泡'));
document.addEventListener('click', () => console.log('5 document 冒泡'));

// 点击按钮,输出顺序:1 → 2 → 3 → 4 → 5

两个容易踩坑的点:第一,在目标元素自身上注册的监听器,无论 capture 传 true 还是 false,都按注册顺序执行,不再区分捕获冒泡;第二,event.target 是实际被点击的元素,event.currentTarget 是当前正在执行监听器的那个元素,事件委托里判断条件要用 target,移除监听器要用 currentTarget 或保存好的引用。

怎么用事件委托利用冒泡?

结论:把监听器绑在父元素上、用 event.target 判断来源,就是事件委托,它依赖的正是冒泡。

典型写法:

document.querySelector('.list').addEventListener('click', e => {
  const li = e.target.closest('li');
  if (!li) return;
  console.log('点击了', li.dataset.id);
});

好处有两个可量化的点:一是 100 个列表项只需 1 个监听器,而不是 100 个;二是后续通过 appendChild 新增的项不需要重新绑定。注意不是所有事件都冒泡——focus、blur、mouseenter、mouseleave 和元素上的 scroll 不冒泡,要委托它们得换用会冒泡的等价事件:focusin、focusout、mouseover、mouseout。

什么时候应该用捕获而不是冒泡?

结论:需要在事件到达目标之前拦截或抢先处理时用捕获,其余场景默认用冒泡即可。

捕获的实用场景包括:在页面最外层监听点击以统一埋点、在拖拽容器上提前阻止内部元素的默认行为、实现点击遮罩关闭弹窗时判断点击是否发生在内容区内。因为捕获阶段先于目标元素上的所有监听器执行,它能保证你的逻辑「先跑一步」。

代价是捕获不像冒泡那样有成熟的委托生态,可读性差一些,团队协作中建议在代码里显式写 { capture: true } 而不是裸传 true,让人一眼看出意图。

stopPropagation 和 preventDefault 有什么区别?

结论:stopPropagation() 管的是事件传播,preventDefault() 管的是浏览器的默认行为,两者互不影响。

stopPropagation() 阻止事件继续向上冒泡(在捕获阶段调用则阻止继续向下传播),但同一元素上的其它监听器照常执行;要连同一元素上的后续监听器一起掐断,用 stopImmediatePropagation()。preventDefault() 只对可取消事件有效(event.cancelable === true),比如阻止 <a> 跳转、阻止表单提交,它不会影响冒泡。

实践中滥用 stopPropagation() 会让外层的事件委托集体失效,排查成本高。更稳的做法是在委托的回调里用 closest() 判断归属,而不是随手阻止传播。

收尾

记住一句话就够了:捕获是「由外向内」,冒泡是「由内向外」,addEventListener 第三参数默认 false 走冒泡,改成 true 走捕获。日常开发优先用冒泡加事件委托,把监听器数量从 N 降到 1;需要在事件到达目标前动手脚时再上捕获。区分清 target 与 currentTarget、stopPropagation 与 preventDefault,这两组概念搞混了,八成的诡异 bug 都能解释得通。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2235.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~