写在前面:本文是撰写的内容是小编个人对于JavaScript事件流机制的一点理解,阐述了包括事件捕获、冒泡以及事件委托等事件流的重点内容,不论是对于要为面试做准备还是想要深入的复习一下JavaScript知识的小伙伴们,希望这篇文章都能帮助到你!
文章内容如有纰漏,欢迎各位大佬指正!
前言
在JavaScript中,当一个事件发生时,它会经历三个阶段:
捕获阶段 -> 目标阶段 -> 冒泡阶段
想象你点击了一个嵌套很深的按钮,这个点击事件会先从最外层的窗口(window)像潜水一样"下沉"到目标按钮(捕获阶段 ),然后在目标按钮上触发(目标阶段 ),最后像气泡一样从按钮"上浮"回窗口(冒泡阶段)。
重点来了,你知道默认情况下,我们添加的事件监听器都是哪个阶段触发的吗?
答案是冒泡阶段。但是这只是JavaScript的默认行为,我们可以手动改变这个行为,按照你的意愿来选择在哪个阶段触发事件的回调函数,比如当你使用addEventListener来绑定事件回调时,你可以通过设置addEventListener的第三个参数来决定回调何时触发!
js
<body>
<div class="grafa">
<div class="father">
<div class="son"></div>
</div>
</div>
</body>
<script>
document.querySelector('.son').addEventListener('click', callback, true)
// 此时事件回调会在捕获阶段触发而不是在冒泡阶段触发!
</script>
一般情况下,当使用document.querySelector('son').addEventListener
来为一个元素绑定事件监听时,我们一般不会设置第三个参数,这个时候第三个参数会默认为false,事件回调会默认在捕获阶段触发。当设置addEventListener
的第三个参数为true时,就可以让监听器在捕获阶段就"抓住"这个事件,相当开启捕获,同时事件回调就不会在捕获阶段触发啦!
JavaScript的事件流过程

因小编疏忽未记录图片具体来源,若涉及侵权,请联系小编,立马删(;´༎ຶД༎ຶ`)🙏(^人^)
一、事件流的三个阶段
在了解具体的事件流过程之前,我们需要提前知道一些具体概念:
- 在整个事件流过程,传递的是事件(
click、error、keyup、change
等等)而不是回调(callback
) - 事件目标(
Event target
)指事件最初触发的那个 DOM 元素(event.target
),是事件流中 "目标阶段" 的核心 - 注意区分
event.target
(实际触发元素)和event.currentTarget
(当前处理事件的元素,通常是绑定事件的元素),在一次事件触发的过程总,前者是始终不变的,而后者指向回调绑定的元素!
下面是事件流三个阶段的具体过程:
1、捕获阶段 --> 从window对象向下传播到目标元素
- 传播方向:从最外层的 window 对象开始,逐级向下传播到目标元素的父级
- 标准:默认情况下,事件监听器不会在这个阶段触发(W3C标准规定)
- 如何改变:设置
addEventListener
的第三个参数设置为 true 就可以启用捕获(该参数不设置时默认为false,代表启用的是冒泡)
2、目标阶段 --> 事件到达目标元素
- 方向:事件到达实际触发事件的元素(即真正绑定了该事件的元素)
- 标准:不论是设置捕获还是冒泡,目标阶段在目标元素的事件处理程序都会触发
- 事件存在默认参数
event
,event.target
指向实际触发事件的元素
3、冒泡阶段 --> 从目标元素向上传播回window对象
- 从目标元素(
event.target
)开始,逐级向上传播到 window 对象 - 标准:默认的事件监听器在这个阶段触发(W3C标准规定)
- 大多数事件都会冒泡(除了少数非冒泡事件如
focus、blur
等)
为什么默认情况下事件监听器不会在捕获阶段触发?
- 兼容性选择,早期浏览器(如 IE)只支持冒泡
- 冒泡阶段更符合开发者的直觉,当点击一个按钮时,我们通常关心的是按钮本身的事件,然后才考虑它的父级(冒泡方向)
- 如果默认启用捕获,浏览器需要在每个事件触发时额外检查捕获阶段的监听器,即使它们很少被使用,这会略微影响性能
二、捕获和冒泡的详解
光说不练假把式,理论讲了这么多,咱们肯定得来个小 demo 试试水!
代码跑起来的瞬间,才是知识点真正 "活" 过来的时候~
下面是小编自己写的一个小demo,希望通过这个小demo的展示可以加深你对于事件流的理解!由于默认标准下事件回调是在冒泡阶段触发的,所以小编没有按照顺序,先展示了大家更加熟悉的冒泡阶段hh
1、事件冒泡
当一个元素接收到事件的时候,会把它接收到的事件传给自己的父级,一直到 window (注意这里传递的仅仅是事件,例如click、focus等等这些事件, 并不传递所绑定的事件函数。)
方向:从事件源 =>最外层的根节点(由内到外)进行事件传播。举例说明👇
html
<body>
<div class="grafa">
<div class="father">
<div class="son"></div>
</div>
</div>
</body>
<script>
document.querySelector('.grafa').addEventListener('click', (e) => {
console.log('在grafa上点击了', e.target);
});
document.querySelector('.father').addEventListener('click', (e) => {
console.log('在father上点击了', e.target);
})
document.querySelector('.son').addEventListener('click', (e) => {
console.log('在son上点击了', e.target);
})
</script>
点击最小的紫色盒子(son),click
事件传播的方向为window -> grafa -> father -> son -> son -> father -> grafa -> window
前面四个阶段是捕获阶段,后面四个阶段则是冒泡阶段。由于此刻这几个事件监听器都默认启用事件冒泡机制,因此捕获阶段不会触发任何盒子的回调函数,冒泡阶段则是按照son -> father -> grafa -> window
的顺序触发回调!执行结果如下:

注意点 :这里为了加深影响,小编特地输出了event.terget
,展示传递的目标元素始终是事件源元素,就算将son
盒子的点击事件去掉,点击son
盒子也会冒泡,且事件源还是son
,如上图一样,不管在哪一个回调打印e.target
,结果都是<div class="son"></div>
!

如上图,在father
元素上触发click
事件的原理也是一样的,尽管son
元素上没有办好顶click
事件,但是由于点击的是son
盒子,所以event.terget
依旧指向它!
如果不希望事件冒泡(也就是不希望点击
son
盒子时其他父元素也会触发相同的事件),在目标元素的事件回调中加入e.stopPropagation()
就可以取消冒泡了!
2、事件捕获
事件捕获: 当鼠标点击或者触发dom
事件时(被触发dom
事件的这个元素被叫作事件源),浏览器会从根节点 => 事件源(由外到内)进行事件传播。
事件捕获(由外到内)与事件冒泡(由内到外)是比较类似的,最大的不同在于事件传播的方向。而浏览器的事件流是从window
对象开始,向下传播到目标元素,再向上传回window
对象(捕获阶段 --> 目标阶段 ---> 冒泡阶段),所以如果同时监听了捕获事件和冒泡事件,那么会先触发捕获事件,再触发冒泡事件,如下例子👇
html
<body>
<div class="grafa">
<div class="father">
<div class="son"></div>
</div>
</div>
</body>
<script>
const grafa = document.querySelector('.grafa');
const father = document.querySelector('.father');
const son = document.querySelector('.son');
grafa.addEventListener('click', (e) => {
console.log('grafa------捕获', e.target);
},true);
father.addEventListener('click', (e) => {
console.log('father------捕获', e.target);
},true);
son.addEventListener('click', (e) => {
console.log('son------捕获', e.target);
},true)
grafa.addEventListener('click', (e) => {
console.log('grafa------冒泡', e.target);
});
father.addEventListener('click', (e) => {
console.log('father------冒泡', e.target);
});
son.addEventListener('click', (e) => {
console.log('son------冒泡', e.target);
})
</script>
点击紫色son
盒子时的运行效果如下,事件的传播方向为window -> grafa -> father -> son -> son -> father -> grafa -> window
,事件回调会在中间的六个阶段被依次触发!所以你会注意到,事件触发的顺序和我的代码顺序是不一样的,因为这是由事件流的触发顺序决定的!

三、事件流的实际应用
看到这里,相信你已经对于JavaScript的事件流机制有一些见解了,下面是对于事件流机制的一些应用,包括利用它带来的便利,或规避它带来的限制!
1、事件委托
事件委托
也称为事件代理
。本质上是利用了浏览器事件冒泡的机制,把子元素的事件都冒泡绑定到父元素上。如果子元素阻止了事件冒泡,那么委托就无法实现,所以子元素必须要保证冒泡。
具体应用示例:像下面这种情况,你在一个father
父盒子里面包裹了大量的son
子盒子,如果你需要对每个son
盒子都绑定一个点击事件回调监听,那么岂不是要手动遍历操作了?而利用事件委托机制,我们只需要在father
父盒子上面绑定点击事件,就可以完成对于所有son
元素的事件监听了!
举个栗子👇
html
<body>
<div class="father">
<div class="son1 box">son1</div>
<div class="son2 box">son2</div>
<div class="son3 box">son3</div>
<div class="son4 box">son4</div>
<div class="son5 box">son5</div>
<div class="son6 box">son6</div>
</div>
</body>
<script>
document.querySelector('.father').addEventListener('click', (e) => {
console.log('触发father的点击事件', e.target);
})
</script>
当我们点击son1
盒子时,我们并没有在son
盒子上绑定事件,但是由于事件冒泡的事件源始终是其所点击的目标元素,所以不管在事件流的哪一个阶段拿到事件对象event
,结果都是事件源对应的元素,以上我们便可以拿到对应的e.target
为son1
:

原理:事件委托不是在每个子节点上都单独设置事件监听器,而是只将事件监听器设置在所有节点的共同父节点上,然后利用事件冒泡始终只传递目标源的机制来影响设置每个子节点。
事件委托的好处:当父元素包裹大量元素时,使用事件委托机制来绑定事件监听器,避免手动给每一个元素都绑定监听器,这样能显著减少内存占用和提高性能!
2、控制事件的默认行为
很多事件在触发之后都会自动携带一些默认行为,这是浏览器的"本能反应",比如:
- 点击
<a>
标签会自动跳转链接 (可能有时候我们希望有个确认框) - 表单提交按钮(
<button type="submit">
)点击之后就会提交表单并刷新页面 (可能需要在提交之前做一些自定义校验、或者重组表单信息等操作) - 点击鼠标右键会弹出浏览器默认菜单 (在一些特定功能下右键可能有别的用处)
- 按下键盘的方向键会让页面滚动 (这也并不一定是我们想要的)
以上这些行为是浏览器内置的,而括号内的行为是我们可能需要自定义交互的,那么为了完成既定的需求,第一步就需要阻止默认行为
阻止默认行为的方式
使用事件对象的preventDefault()
方法,比如:
js
// 阻止链接跳转
document.querySelector('a').addEventListener('click', (e) => {
e.preventDefault(); // 阻止默认跳转
console.log('链接被点击,但不会跳转');
// 处理完成后手动跳转
// const url = e.target.href; // 获取链接的href属性
// window.location.href = url; // 跳转到目标地址
});
// 阻止表单提交刷新
document.querySelector('form').addEventListener('submit', (e) => {
e.preventDefault(); // 阻止默认提交
console.log('表单内容将通过AJAX手动提交');
});
⚠️ 请注意:
preventDefault()
只阻止默认行为,不影响事件流(事件该冒泡还是会冒泡)。- 有些事件本身就不能阻止默认行为(比如
scroll
),这由浏览器规定。
3、终止事件流的方式
事件流默认会完整经历 "捕获→目标→冒泡" 三个阶段。但实际开发中,我们可能需要在某个节点 "截住" 它,避免事件继续传递 ------ 这就需要终止事件流。
(1)e.stopPropagation()
- 阻止事件向下传播
小编还是拿文章上面的例子,我们只在对于son
元素的监听中增加了e.stopPropagation()
这一行代码,来看看会有什么效果吧!
html
<body>
<div class="grafa">
<div class="father">
<div class="son"></div>
</div>
</div>
</body>
<script>
document.querySelector('.son').addEventListener('click', (e) => {
console.log('在son上点击了', e.target);
e.stopPropagation(); // 阻止事件冒泡
})
document.querySelector('.father').addEventListener('click', (e) => {
console.log('在father上点击了', e.target);
})
document.querySelector('.grafa').addEventListener('click', (e) => {
console.log('在grafa上点击了', e.target);
});
</script>

此时我们再点击son
盒子(墨绿色)时会发现,只有绑定到该盒子上面的事件回调执行了,这是因为当son
盒子上的事件回调触发之后,我们调用了事件对象e
上面的stopPropagation()
方法,这个方法会阻止事件的传播,让事件流在当前元素的当前阶段停下,不再传递给后续的元素或阶段!
(2)e.stopImmediatePropagation()
- 更为彻底的终止
相比于stopPropagation
方法,stopImmediatePropagation
方法不仅会终止事件向下传播,而且还会让当前元素上所有的同类事件的后续处理函数都失效!
html
<body>
<div class="box"></div>
</body>
<script>
document.querySelector('.box').addEventListener('click', (e) => {
console.log('在box上点击了 - 1', e.target);
e.stopImmediatePropagation() //更彻底的终止
})
document.querySelector('.box').addEventListener('click', (e) => {
console.log('在box上点击了 - 2', e.target);
})
document.querySelector('.box').addEventListener('click', (e) => {
console.log('在box上点击了 - 3', e.target);
})
</script>

此时点击这个盒子,仅第一个回调执行,因为在该回调中调用了stopImmediatePropagation
方法,导致当前盒子上所有的同类时间的后续处理函数都失效了!
总结:把
JavaScript
的事件流想象成一条磅礴向前的河流,从高山(window
)顺流而下、经过一个个闸口(层层包裹的元素)、逆流而上回到高山上。stopPropagation
方法就像在河道中间筑一道大坝,让事件流直接在当前闸口的当前阶段就停下,而stopImmediatePropagation
方法更像直接断流,不仅影响后面的闸口,就连当前闸口的其它同类也会立刻罢工...
以上为本文的全部内容,小编技术水平有限,如果文章存在不妥指出,恳请指出,小编虚心求教,感谢各位小伙伴!