DOM事件流——捕获与冒泡
网页是由一个一个元素组成的,正如我们肉眼所见,网页上的元素存在包含关系,简单的点击又怎么确定到底谁来触发响应呢?想象一下,在纸上画了两个大小不同的同心圆,然后用手指指向它里面的圆,那么你能说了指向的圆没有包含外面的圆吗?显然不是的,为了解决这个问题,出现了事件流。
DOM事件流三阶段
当用户与HTML页面交互(如点击按钮、移动鼠标等)时,就会产生事件,而DOM结构是一个树型结构,当一个HTML元素产生一个事件时,该事件会在元素节点与根结点之间的路径传播,路径所经过的结点都会收到该事件,这个传播过程可称为DOM事件流。DOM事件流主要分为三个阶段:捕获阶段、目标阶段、冒泡阶段。
1、捕获阶段Capturing 捕获是由网景公司最先提出的,事件从文档的根节点开始,向下传播至目标元素。在这个过程中,事件会经过目标元素的父元素、祖父元素等,但通常不会在这个阶段触发事件监听器,除非特别指定。这可以用于在事件传播到特定子元素之前,先进行一些全局性的处理或检查。
2、目标阶段Target 当事件到达目标元素时,事件会被触发,并执行绑定在该元素上的事件监听器。监听器就会按照它们被添加到元素上的顺序被调用,不管是捕获还是冒泡事件,只会按添加的顺序执行。(注意⚠️:在最新的浏览器版本中如果同时存在默认和冒泡,都会先执行捕获!!!)
3、冒泡阶段Bubbling:冒泡是IE最早提出的,事件从目标元素开始,向上冒泡至根节点。在这个过程中,事件同样会经过目标元素的父元素、祖父元素等,且可以在这些元素上触发事件监听器。这通常用于实现事件的委托(Event Delegation),即在一个父元素上监听子元素的事件,从而减少事件监听器的数量,提高性能。在实际开发中我们使用跟多的也是冒泡,很少用到捕获。
代码示例,单击白色inner盒子,执行结果如下:
<!DOCTYPE html>
<html>
<head>
<title>DOM事件流</title>
</head>
<body>
<div class="outer">
<div class="inner"></div>
</div>
</body>
<script>
// 获取DOM元素
let outer = document.querySelector('.outer')
let inner = document.querySelector('.inner')
// 绑定事件
outer.addEventListener('click', () => console.log('outer冒泡阶段'), false) // 5
outer.addEventListener('click', () => console.log('outer捕获阶段'), true) // 1
inner.addEventListener('click', () => console.log('inner冒泡阶段'), false) // 4
inner.addEventListener('click', () => console.log('inner捕获阶段方法1'), true) // 2
inner.addEventListener('click', () => console.log('inner捕获阶段方法2'), true) //3
</script>
<style>
.outer {
display: flex;
align-items: center;
width: 300px;
height: 300px;
background-color: pink;
}
.inner {
margin: 0 auto;
width: 200px;
height: 200px;
background-color: white;
}
</style>
</html>
可见,事件的整体顺序是:祖先元素捕获 -> 目标元素捕获 -> 目标元素冒泡 -> 祖先元素冒泡(⚠️老版本浏览器目标元素可能按绑定顺序执行)
无法冒泡的事件
基本上只有onload
、unload
、focus
、blur
、submit
和change
事件是不支持冒泡的
阻止冒泡
为什么要阻止冒泡
1、提高性能和效率:如果多个元素(如父元素和子元素)都绑定了相同或相似的事件处理函数,并且这些函数都执行了相似的操作,那么当事件发生时,这些函数都会被调用,从而导致重复的计算或处理,浪费资源。通过阻止事件冒泡,可以避免这种不必要的重复处理,提高代码的性能和效率。
2、控制事件传播范围:有时候,我们可能只希望事件在特定的元素上触发,而不希望它继续传播到父元素或祖先元素上。通过阻止事件冒泡,我们可以精确地控制事件的传播范围,确保事件只在目标元素上触发。
3、防止事件的冲突和干扰:在复杂的前端应用中,可能存在多个嵌套的元素,它们都有自己的事件处理逻辑。如果事件冒泡到了父元素或祖先元素,可能会触发其他元素上的事件处理函数,导致事件的冲突和干扰。
如何阻止冒泡
1、stopPropagation()
标准方法:利用事件对象里面的stopPropagation()
方法
outer.addEventListener('click', () => console.log('outer冒泡阶段'), false)
outer.addEventListener('click', () => console.log('outer捕获阶段'), true)
inner.addEventListener('click', () => console.log('inner冒泡阶段'), false)
inner.addEventListener(
'click',
() => {
console.log('inner捕获阶段方法1')
event.stopPropagation()
},
true
)
执行结果如下,在捕获阶段使用stopPropagation()
冒泡都被阻止了
2、cancelBubble
兼容性方法:虽然stopPropagation()
已被大多数浏览器支持,但IE6-8需要利用事件对象的cancelBubble
属性阻止冒泡,即设置:window.event.cancelBubble = true;
3、在事件处理函数中返回false
:这个方法很多地方都有提到,但实践起来并不奏效,首先他限定事件处理函数是通过HTML属性(如onclick
)或者某些特定的JavaScript库(如jQuery)以特定方式注册的,其次不同浏览器处理方式也不一样,最新的浏览器中大多数已经无法奏效了
4、设置CSS属性:虽然CSS本身不直接提供阻止事件冒泡的功能,但有时候通过改变元素的显示方式(如display: none;
)或可交互性(如pointer-events: none;
),可以间接影响事件的传播,这个方法具有局限性