面试官:请你谈谈js中事件代理机制🚀🚀

1,110 阅读7分钟

浅谈事件代理

事件代理也可以称作事件委托,是前端开发中一种常用的事件处理技术,它允许我们将事件监听器设置在父元素上,而不是直接附加到实际需要响应事件的子元素上。这种方法利用了事件冒泡(Event Bubbling)机制,即当一个元素上的事件发生时,该事件会向上冒泡至其祖先元素。通过在较高层级的元素上监听事件,我们可以统一处理所有子元素上的同类型事件,从而减少事件监听器的数量,提高性能。 光靠理论来讲远远不够,我们来通过一个例子来生动的描述吧!!

<!DOCTYPE html>  
<html lang="en">  
<head>  
<meta charset="UTF-8">  
<title>Title</title>  
</head>  
<body>  
<ul>  
<li>1</li>  
<li>2</li>  
<li>3</li>  
</ul>  
  
  
</body>  
  
<script>  
let lis= document.querySelectorAll("li")  
//console.log(lis);  
lis.forEach((li,i)=>{  
li.addEventListener("click",()=>{  
console.log(lis[i].innerHTML);  
})  
})  
</script>  
</html>


上述代码可以让我们点击一个li标签后控制台就能打印出它里边的内容。但这样其实并不够优雅。如果上班写这样的代码的话没有啥性能上的提升,这可能就是你的跑路代码了--哈哈。那么如何写出优雅的代码呢,这就要聊到我们的主角js中的事件代理机制了!!给我一盏茶的时间让我从底层娓娓道来~~

js中的事件流

下面我用一个小demo演示下


<head>  
<meta charset="UTF-8">  
<title>Title</title>  
<style>  
#grand{  
width: 400px;  
height: 400px;  
background-color: aqua;  
}  
#parent{  
width: 300px;  
height: 300px;  
background-color: blueviolet;  
}  
#child{  
width: 200px;  
height: 200px;  
background-color: chartreuse;  
}  
</style>  
</head>  
<body>  
<div id="grand">  
  
<div id="parent">  
<div id="child">  
  
</div>  
</div>  
</div>  
<script>  
let grand=document.getElementById("grand");  
let parent=document.getElementById("parent")  
let child=document.getElementById("child")  
grand.addEventListener('click',()=>{  
console.log("grand")  
})  
parent.addEventListener('click',()=>{  
console.log("parent")  
})  
child.addEventListener('click',()=>{  
console.log("child")  
})  
  
</script>  
</body>

屏幕截图 2024-10-07 220849.png

当我点击紫色区域后查看控制台:

image.png

为什么会出现这种奇怪的现象呢。按道理来说我没有点击青色的区域不会打印出grand啊! 这就不得不谈到js里边的事件流了

事件流中的捕获阶段

简而言之:事件流的捕获阶段(Event Capturing Phase)是事件处理机制中的一个重要组成部分,它定义了事件如何从文档的根节点(通常是documentwindow对象)开始,沿着DOM树逐层向下传播,直到到达目标元素的过程。 当我点击紫色容器时,其实是先作用于青色容器。(青色容器包含紫色容器)然后再传播到紫色容器上。 由此可见捕获阶段--点击行为发生在目标事件,其实一开始触发在window身上,然后一路蔓延到html、body、......直到蔓延到目标事件上。

事件流中的目标阶段

当从window处蔓延到目标阶段后,这个蔓延过程其实没有产生任何执行流程,在捕获阶段不会触发事件的发生,来到了目标容器后就会触发目标容器绑定的事件。

事件流中的冒泡阶段

总而言之:是事件从目标处往window上传播。我们可以举个形象的例子,目标事件所处的位置好比是在深处, 当它往window上传播时候,好比是从深处往浅处传播。跟气泡冒泡一样。值得注意的是:js中事件触发默认就发生在冒泡阶段

冒泡阶段触发的缘由

image.png 当我们只是点赞的话并不会进入这篇文章里边。如果不是冒泡阶段触发点击事件而是捕获阶段触发点击事件的话那么就会发生:当我们点击赞的话那么就会直接进入文章里边。那么我们转念一想,实际开发当中我们可能会想让事件发生在捕获阶段,那么如何做到这一点呢?

一个参数改变事件发生

<script>  
let grand=document.getElementById("grand");  
let parent=document.getElementById("parent")  
let child=document.getElementById("child")  
grand.addEventListener('click',()=>{  
console.log("grand")  
},true)  
parent.addEventListener('click',()=>{  
console.log("parent")  
},true)  
child.addEventListener('click',()=>{  
console.log("child")  
},true)  
</script>

image.png

其实addEventListener()里边接受三个参数, 第三个参数不写的话那么默认为false,写true之后事件就会在捕获阶段发生, 由此打印出来的结果与之前完全相反。原理就是当为true后,捕获阶段时事件流经过该容器,该容器绑定的事件就会触发。

我们来再实操一下看看效果

<script>  
let grand=document.getElementById("grand");  
let parent=document.getElementById("parent")  
let child=document.getElementById("child")  
grand.addEventListener('click',()=>{  
console.log("grand")  
},true)  
parent.addEventListener('click',()=>{  
console.log("parent")  
},false)  
child.addEventListener('click',()=>{  
console.log("child")  
},false)  
</script>

image.png

阻止事件流的传播

有时候我们并不想让事件传播下去

image.png 当我们点了赞之后,我们并不希望事件流传播下去造成文章被打开。那这里就不得不提阻止事件流传播的手段了。话不多说我们上实操!!

<script>  
let grand=document.getElementById("grand");  
let parent=document.getElementById("parent")  
let child=document.getElementById("child")  
grand.addEventListener('click',()=>{  
console.log("grand")  
},false)  
parent.addEventListener('click',()=>{  
console.log("parent")  
},false)  
child.addEventListener('click',(e)=>{  
console.log("child")  
e.stopProagation();
},false)  
</script>

image.png

由此可看出——当我们调用事件参数的stopPropagation()后成功阻止了冒泡事件(不含当前自身,自身的事件还是会触发) 类似的我们也可以发现事件参数的stopPropagation()也会阻止捕获阶段(当我们换成true后),其实除了stopPropagation()外还有一种stopImmediatePropagation()

<script>  
let grand=document.getElementById("grand");  
let parent=document.getElementById("parent")  
let child=document.getElementById("child")  
grand.addEventListener('click',()=>{  
console.log("grand")  
},false)  
parent.addEventListener('click',(e)=>{  
e.stopImmediatePropagation();
console.log("parent")  
},false)  
parent.addEventListener('click',()=>{  
console.log("parent2")  
},false)  
child.addEventListener('click',(e)=>{  
console.log("child")  
},false)  
</script>

运行结果如下 image.png 由此可以看出e.stopImmediatePropagation();可以阻止事件流的传播,并且还能阻止同一个容器绑定相同的事件,注意!!(不能阻止同一个容器绑定的不同事件)

DOM0 VS DOM2

常见的DOM0事件有onclick......那我们来看看它的效果吧!

<body>  
<div id="grand" onclick="handleGrand()">  
<div id="parent" onclick="handleParent()">  
<div id="child">  
  
</div>  
</div>  
</div>  
  
<script>  
function handleGrand(){  
console.log('grand')  
}  
function handleParent(){  
console.log('parent')  
}  
</script>

image.png 可见DOM0事件也有事件流中的捕获阶段,和冒泡阶段。但值得注意的是我们无法人为修改事件在哪个阶段触发,它不像addEventListener那样有第三个参数可以修饰。因此这是固定死了的事件的发生只能在冒泡阶段发生,!!另外同一个容器不能绑定两个相同的事件,第二个绑定的事件不会触发。

事件代理

开篇我们浅谈了下事件代理,现在我们来看看它的真实面纱吧 把元素身上需要响应的事件,委托到另外一个元素(父元素)上。

<script>  
let lis= document.querySelectorAll("li")  
//console.log(lis);  
lis.forEach((li,i)=>{  
li.addEventListener("click",()=>{  
console.log(lis[i].innerHTML);  
})  
})  
</script>  

上边的代码实际上如果我们在公司写这种代码的话,真的会卷铺盖走人!!这种写法很占内存。我们试想一下,这种做法一次性把所有li拿出来,然后把每个li都绑定点击事件。li.addEventListener("click",()=>{}),addEventListener是一个监听行为也称为订阅行为它为箭头函数这该函数体订阅了一个点击事件,鼠标点击行为发布这个事件,函数体就触发了。当我们不点击的时候,那么就会处于一直订阅的状态。这段代码v8会先把这段代码先执行出来,v8会先在调用栈或堆里边提前记录后有一些函数体要触发,如果li有一百个上万个呢这会很占内存,栈和堆都会占用运行内存。那么为了避免这种情况发生,我们就可以用到事件委托了。

<!DOCTYPE html>  
<html lang="en">  
<head>  
<meta charset="UTF-8">  
<title>Title</title>  
</head>  
<body>  
<ul>  
<li>1</li>  
<li>2</li>  
<li>3</li>  
</ul>  
</body>  
<script>  
let ul=document.querySelector('ul')
    ul.addEventListener("click",function(e){
          console.log(e.target.innerText);
    })
</script>  
</html>

上述代码就得到了性能优化,效果跟之前的一样。当我们点击一个li的时候第一个li就是目标处,事件流就会来到目标阶段,这时候我们就可以用描述点击行为的事件参数e(正是因为js中有事件流这个机制,事件参数能帮我们找到事件触发处在哪)了。换句话说我们不再需要获取li,这就是事件委托。 !!但是值得注意的是不是什么事件都存在事件流,也就是说不能进行事件委托比如input框上的focus(聚焦)因为这是input框独有的外层容器不存在focus事件。同理还有blur(失焦) 事件。 好啦今天的分享就到这啦!!!请大家持续关注!!