前言
首先js的事件流程如下:
1、捕获阶段——事件从window处往目标处传播
2、目标阶段——在目标处触发事件
3、冒泡阶段——事件从目标处往window处传播
js中的事件默认都在冒泡阶段触发
js事件流
js事件流描述的其实就是js事件传播的一个顺序,上面的答案就是对应的三个阶段,捕获阶段、目标阶段、冒泡阶段。我来给大家举个例子来理解一下:
咱们先来三个div盒子,层层嵌套grand>parent>box
<div id="grand">
<div id="parent">
<div id="child"></div>
</div>
</div>
<style>
#grand{
width: 300px;
height: 300px;
background-color: red;
}
#parent{
width: 200px;
height: 200px;
background-color: blue;
}
#child{
width: 100px;
height: 100px;
background-color: green;
}
</style>
再加上一些样式颜色和宽度,grand为最大的红色盒子,parent为第二大的蓝色盒子,child为最小的绿色盒子,效果如下:
咱们再给这三个盒子都绑定一个点击事件,从大到小绑定,并且每个点击事件都会打印自己的id
let grand = document.getElementById("grand");
let parent = document.getElementById("parent");
let child = document.getElementById("child");
grand.addEventListener("click", function(){
console.log("Grand");
})
parent.addEventListener("click", function(){
console.log("Parent");
})
child.addEventListener("click", function(){
console.log("Child");
})
我们现在在grand容器内点击,就一定会打印grand,在parent内点击就一定会打印parent,在child内点击就一定会打印child,由于child被parent和grand包含,点击child,就三个打印都会触发,那么这个触发的顺序是什么样的呢?这就是咱们要讨论的话题,js事件触发的顺序就是js事件流。
咱们先看下他会如何打印,是从里往外还是从外往里?
可以看到,浏览器输出的顺序是从里往外的,从里往外就是对应着js事件流第三步,冒泡事件。冒泡事件是什么?又为何直接到第三步了?往下看
捕获事件
我们点击最里面的容器的时候,会顺带点击到中间的容器以及最外面的容器,js是这样的,事件传播顺序一定是从外向内,最外层是window,然后是html,然后是body,然后就是grand,parent,child,很好理解,就是从外向内。
捕获的含义其实就是父容器包裹一个子容器,捕获的过程碰到的事件就是捕获事件。
冒泡事件
当我们到达了事件触发处的时候,从事件触发处往window上传播。这就是从内向外。冒泡也非常的形象,从里往外,水里的泡泡因为压强的原因,深处的泡泡上浮的过程就是越来越大。刚好对应着这里的div盒子,从里向外,从小到大。
冒泡过程中碰到的事件我们就称之为冒泡事件
js事件流是先从外向里走一遍,在默认情况下,这个被称之为捕获的过程,是不会去触发事件的,到达了目标阶段后,开始往外冒泡,这个过程碰到的事件就是会触发的,这才有了刚才的打印顺序。
既然都说默认了,肯定有方法去更改这个js事件流,如何实现在捕获阶段触发呢?这个时候就要谈到addEventListener的第三个参数了
addEventListener的第三个参数
咱们看看上面绑定事件时的addEventListener
grand.addEventListener("click", function(){
console.log("Grand");
})
第一个参数就是事件名,第二个参数是回调函数,第三个参数可选,他是个布尔值,控制该事件是在捕获过程触发的还是在冒泡过程触发的,默认为false,所以false就代表着冒泡触发,true就代表让该事件在捕获过程被触发
所以我现在给容器最大的grand加上第三个参数为true,那么grand这个容器的事件触发将会被改为捕获阶段触发
grand.addEventListener("click", function(){
console.log("Grand");
},true)
最终点击最小的容器,触发顺序为,捕获阶段先触发最大的grand,然后到达了最小的容器后开始触发默认的冒泡事件,先是最小的child,然后是第二大的parent
现实中还有另外一个很常见的情形,就是我点击子容器,并不希望触发父容器。就拿掘金为例,我可以在首页给一个文章点赞,而不会进入文章。
这个效果的实现我们就需要看下事件event原型中的一个方法:event.stopPropagation了,打印下看看
grand.addEventListener('click', (e) => {
console.log(e)
}, true)
event.stopPropagation和event.stopImmediatePropagation
Propagation就是传播的意思,所以stopPropagation就是阻止传播,并且冒泡和捕获阶段都可以进行阻止
要实现只触发子容器的点击事件很简单,我们只需要保证大家都是默认的冒泡事件,然后到了事件触发处,也就是最小的容器,开始冒泡给他阻止掉就可以,所以我们给最小的容器加个阻止传播就可以了
grand.addEventListener("click", function(){
console.log("Grand");
})
parent.addEventListener("click", function(){
console.log("Parent");
})
child.addEventListener("click", function(){
console.log("Child");
e.stopPropagation()
})
现在再聊下event.stopImmediatePropagation,event.stopPropagation能实现的效果,他也能实现,区别在于它可以阻止同一个dom结构绑定的多个相同事件,不同事件的不行
刚才讲的效果你可以自己换成immediate再试一遍,效果是一样的。
我们现在只给最小的容器绑定多个事件
child.addEventListener("click", function(e){
console.log("Child");
})
child.addEventListener("click", function(e){
console.log("Child2");
})
因为代码从上到下的执行缘故,他会打印child,child2
我们现在给child加上一个immediate阻止
child.addEventListener("click", function(e){
console.log("Child");
e.stopImmediatePropagation()
})
child.addEventListener("click", function(e){
console.log("Child2");
})
他就会阻止后面的相同事件,仅打印child
应用场景:项目是多个人开发的,别人也可以对该dom结构绑定点击事件,你可以给自己添加一个阻止事件,这样就不会引起冲突
当然,相同dom结构绑定不同的事件,是无法阻止的
child.addEventListener("click", function(e){
console.log("Child");
e.stopImmediatePropagation()
})
child.addEventListener("mouseleave", function(e){
console.log("Child2");
})
这里我点击chilld后,再鼠标移除child依旧是可以触发鼠标移出事件的。因此这个方法只能阻止同一dom绑定的多个相同事件
结尾
以上这些知识点就是js非常基础的事件流,整个流程是先捕获后冒泡,触发过程默认情况是冒泡,也就是像泡泡一样,从里到外,想要改变成为捕获传播就需要动用addEventListener的第三个参数,将其设置成true就是捕获事件,想要阻止事件传播就要调用stopPropagation或者stopImmediatePropagation方法