[ 前端JavaScript的事件流机制 ] - 捕获、冒泡及委托
写在前面:本文是撰写的内容是对于JavaScript事件流机制的解析,详细阐述了包括事件捕获、冒泡以及事件委托在内的事件流重点内容,不论是对于要为面试做准备还是想要深入的复习一下JS的知识的小伙伴,希望这篇文章都能帮助到你!
目录
- 前言:
- 一、事件流的基本概念
- 二、事件冒泡和事件捕获
- 1、事件冒泡
- 2、事件捕获
- 三、事件委托
前言:
在JavaScript中,当一个事件发生时,它会经历三个阶段:捕获、目标和冒泡。想象你点击了一个嵌套很深的按钮,这个点击事件会先从最外层的窗口(window)像潜水一样"下沉"到目标按钮(捕获阶段),然后在目标按钮上触发(目标阶段),最后像气泡一样从按钮"上浮"回窗口(冒泡阶段)。
默认情况下,我们添加的事件监听器都是在冒泡阶段触发的,就像气泡上浮一样。但如果你使用的是document.querySelector('元素').addEventListener
,可以通过设置addEventListener
的第三个参数为true,让监听器在捕获阶段就"抓住"这个事件,也就是开启捕获。
事件委托(代理)是一种聪明的技巧,它利用了事件冒泡的特性。比如一个列表有100个项目,我们不需要给每个项目都添加点击事件,只需要在它们的父元素上添加一个监听器。当点击任何一个子项目时,事件会冒泡到父元素,我们通过检查事件的目标(event.target)就能知道是哪个具体项目被点击了。这样做既节省内存,又能自动处理动态新增的元素,非常高效实用!
一、事件流的基本概念
JS的事件流描述了一个元素所绑定的事件从触发到处理结束的完整过程,包含以下三个阶段:
1、捕获阶段 --> 从window对象向下传播到目标元素
- 从最外层的 window 对象开始,逐级向下传播到目标元素的父级
- 默认情况下,事件监听器不会在这个阶段触发(W3C标准规定)
- 设置
addEventListener
的第三个参数设置为 true 就可以启用捕获(该参数不设置时默认为false,代表启用的是冒泡)
2、目标捕获阶段 --> 事件到达目标元素
- 事件到达实际触发事件的元素(即真正绑定了该事件的元素)
- 无论是否设置了捕获,都会触发目标元素上的事件处理程序
- 事件存在默认参数
event
,event.target
指向实际触发事件的元素
3、冒泡阶段 --> 从目标元素向上传播回window对象
- 从目标元素开始,逐级向上传播到 window 对象
- 大多数事件都会冒泡(除了少数如 focus、blur 等)
- 默认的事件监听器在这个阶段触发
为什么默认情况下事件监听器不会在捕获阶段触发?
- 兼容性选择,早期浏览器(如 IE)只支持冒泡
- 冒泡阶段更符合开发者的直觉,当点击一个按钮时,我们通常关心的是按钮本身的事件,然后才考虑它的父级(冒泡方向)
- 如果默认启用捕获,浏览器需要在每个事件触发时额外检查捕获阶段的监听器,即使它们很少被使用,这会略微影响性能
二、事件冒泡和事件捕获
1、事件冒泡
当一个元素接收到事件的时候,会把他接收到的事件传给自己的父级,一直到 window (注意这里传递的仅仅是事件,例如click、focus等等这些事件, 并不传递所绑定的事件函数。)
方向:从事件源 =>最外层的根节点(由内到外)进行事件传播。举例说明👇
<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 -> father -> grafa -> window
前面四个阶段是事件捕获阶段,后面则是事件冒泡阶段。由于此刻这几个事件监听器都默认启用事件冒泡机制,因此捕获阶段不会触发任何盒子的回调函数,冒泡阶段则是按照son -> father -> grafa
的顺序触发回调!执行结果如下:
注意点:传递的目标元素始终是事件源元素,就算将son
盒子的点击事件去掉,点击son
盒子也会冒泡,且事件源还是son
,如上图一样,不管在哪一个回调打印e.target
,结果都是<div class="son"></div>
!
如上图,在father
元素上触发click
事件的原理也是一样的!
如果不希望事件冒泡(也就是不希望点击
son
盒子时其他父元素也会触发相同的事件),在目标元素的事件回调中加入e.stopPropagation()
就可以取消冒泡了!
2、事件捕获
事件捕获: 当鼠标点击或者触发dom事件时(被触发dom事件的这个元素被叫作事件源),浏览器会从根节点 =>事件源(由外到内)进行事件传播。
事件捕获(由外到内)与事件冒泡(由内到外)是比较类似的,最大的不同在于事件传播的方向。而浏览器的事件流是从window对象开始,向下传播到目标元素,再向上传回window对象(捕获阶段 --> 目标捕获阶段 —> 冒泡阶段),所以如果同时监听了捕获事件和冒泡事件,那么会先触发捕获事件,再触发冒泡事件,如下例子👇
<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
,事件回调会在中间的六个阶段被依次触发!
三、事件委托
事件委托
也称为事件代理
。本质上是利用了浏览器事件冒泡的机制,把子元素的事件都冒泡绑定到父元素上。如果子元素阻止了事件冒泡,那么委托就无法实现,所以子元素必须要保证冒泡。
原理:事件委托不是在每个子节点上都单独设置事件监听器,而是只将事件监听器设置在所有节点的共同父节点上,然后利用事件冒泡始终只传递目标源的机制来影响设置每个子节点。
举个栗子👇
<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
:
事件委托的好处:当父元素包裹大量元素时,使用事件委托机制来绑定事件监听器,避免手动给每一个元素都绑定监听器,这样能显著减少内存占用和提高性能!
以上为本文的全部内容,小编技术水平有限,如果文章存在不妥指出,恳请指出,小编虚心求教,感谢各位小伙伴!