js的事件代理

288 阅读6分钟

前言

首先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为最小的绿色盒子,效果如下:

image-20240829200656614

咱们再给这三个盒子都绑定一个点击事件,从大到小绑定,并且每个点击事件都会打印自己的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事件流

咱们先看下他会如何打印,是从里往外还是从外往里?

image-20240829201431387

可以看到,浏览器输出的顺序是从里往外的,从里往外就是对应着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

image-20240829203138202

现实中还有另外一个很常见的情形,就是我点击子容器,并不希望触发父容器。就拿掘金为例,我可以在首页给一个文章点赞,而不会进入文章。

GIF 2023-12-30-星期六 10-37-36.gif

这个效果的实现我们就需要看下事件event原型中的一个方法:event.stopPropagation了,打印下看看

grand.addEventListener('click', (e) => { 
    console.log(e)
}, true)

image-20240829203759788

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()
        })

image-20240829204034590

现在再聊下event.stopImmediatePropagationevent.stopPropagation能实现的效果,他也能实现,区别在于它可以阻止同一个dom结构绑定的多个相同事件,不同事件的不行

刚才讲的效果你可以自己换成immediate再试一遍,效果是一样的。

我们现在只给最小的容器绑定多个事件

child.addEventListener("click", function(e){
            console.log("Child");      
        })
child.addEventListener("click", function(e){
            console.log("Child2");
        })

因为代码从上到下的执行缘故,他会打印child,child2

image-20240829204530016

我们现在给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方法