前言
在前端开发中,事件处理是非常重要的一部分。随着应用变得越来越复杂,页面上的元素数量也会逐渐增加。如果为每一个元素单独绑定事件监听器,不仅会让代码变得冗长,还会导致性能下降。这时,事件代理(Event Delegation)就成为了一种优化方案。本文将通过几个具体的示例来解释什么是事件代理,并帮助理解其工作原理。
什么是事件代理?
事件代理是一种在父级元素上监听事件,从而间接管理子级元素事件的技术。这种方法利用了事件冒泡的原理:事件会从最深的节点开始逐级向上冒泡,直到达到最顶层的节点。通过在较高级别的元素上监听事件,我们可以捕获发生在任何子元素上的事件,而无需为每个子元素单独绑定事件处理器。
事件代理的好处
- 减少事件处理器:只需要为一个父元素绑定事件处理器,而不是为每一个子元素。
- 易于维护:当子元素动态增加或减少时,不需要重新绑定事件。
- 性能提升:减少事件处理器的数量可以降低内存消耗和提高性能。
事件代理的工作原理
事件代理的关键在于理解事件冒泡机制。当用户点击页面上的某个元素时,事件会首先在这个元素上触发,然后向上冒泡,直到到达文档的根节点。我们可以利用这一点,在较高层级的元素上监听事件,然后通过事件对象来判断实际触发事件的元素。
正文
让我们通过几个具体的例子来更好地理解事件代理的实现方式。
示例一:为列表项单独绑定事件
场景:如果有一个列表,里面有几十个<li>
标签,让你点击哪个就显示哪个的内容你会怎么做?
诶,这时我们会想,这还不简单吗,每个都绑定点击事件咯,再把它标签的内容展示出来不就好了,下面代码就是如此:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件代理示例</title>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
let lis = document.querySelectorAll('li');
lis.forEach((li, i) => {
li.addEventListener('click', function () {
console.log(this.innerText);
});
});
</script>
</body>
</html>
这里简略点就写了三个<li>
,那那么多<li>
每个都绑定那不是太麻烦嘛,可不可以简单一点的,诶,还真有,下面为用了事件代理的升级版:
示例二:使用事件代理为列表项绑定事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件代理示例</title>
</head>
<body>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
let ul=document.querySelector('ul')
ul.addEventListener('click',function(e){
console.log(e.target.innerText)
})
</script>
</body>
</html>
效果:
可以看到,我们点击不同的
<li>
一样可以实现我们想要的效果,我们仅在 <ul>
元素上添加了一个点击事件监听器。当用户点击任意一个 <li>
时,事件会冒泡到 <ul>
,然后我们在事件处理函数中检查 e.target
是否是我们想要的目标元素,这里是 <li>
。如果是,我们就打印出该 <li>
的文本内容。这种方式很好减少了事件处理器的数量,提高了性能。
如果没理解没关系,我们看下面这个例子就明白了。
示例三:多层嵌套元素的事件代理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#grand{
width:400px;
height: 400px;
background-color: rgba(0, 0, 255, 0.527);
}
#parent{
width: 300px;
height: 300px;
background-color: red;
}
#child{
width: 200px;
height: 200px;
background-color: yellow;
}
</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", function(e){
console.log("grand");
})
parent.addEventListener("click", function(e){
console.log("parent");
})
child.addEventListener("click", function(e){
console.log("child");
})
</script>
</body>
</html>
在这个示例中,我们有一个三层嵌套的结构。可以看到,当点击 grand元素(蓝色部分)时,打印出了grand;点击parent元素(黄色部分)时,打印出了parent、grand;点击child元素(红色部分)时,打印出了child、parent、grand。
要明白原理我们就要了解一下js
中的事件流了,js
中的事件流分为三个阶段:
- 捕获阶段:事件从最外层的元素(如 window 对象)开始向内传播,直到达到目标元素。
- 目标阶段:事件在目标元素上触发。
- 冒泡阶段:事件从目标元素开始向外传播,直到达到最外层元素。
重点:js
中的事件默认在冒泡阶段触发
所有当我们点击child元素(红色部分)时,js
就会从window开始捕获事件,到html,再到body,再到grand(有一个点击事件),再到parent(有一个点击事件),最后到目标元素child(有一个点击事件),ok找到之后,开始触发,就会有我们在控制台看到的效果:child,再parent,再grand。所有上面点击<li>
标签就会冒泡到<ul>
触发事件。
我们知道这个之后,我们再来看看:
grand.addEventListener("click", function(e){
console.log("grand");
},false)
parent.addEventListener("click", function(e){
console.log("parent");
},true)
child.addEventListener("click", function(e){
console.log("child");
},false)
其他代码不变,就加了true,false,false是默认的,不写默认为false表示在冒泡阶段触发,写true表示在捕获阶段就触发,所有当点击child时,打印的是parent、child、grand。
再修改来看看:
grand.addEventListener("click", function(e){
console.log("grand");
},false)
parent.addEventListener("click", function(e){
e.stopPropagation() // 点击child部分,打印child、parent、parent1
// e.stopImmediatePropagation()
console.log("parent");
},false)
parent.addEventListener("click", function(e){
console.log("parent1");
})
child.addEventListener("click", function(e){
console.log("child");
},false)
parent.addEventListener("click", function(e){
// e.stopPropagation()
e.stopImmediatePropagation() // 点击child部分,打印child、parent
console.log("parent");
},false)
- e.stopPropagation(): 阻止事件流的传播,会一直冒泡到这个标签,里面及本身的事件都会触发,外面的都不会触发
- e.stopImmediatePropagation(): 阻止事件流的传播+阻止同一个容器绑定多个相同的事件
用处
: 我们看这个图片:
我们点击js检测页面空闲
这个大的部分是不是会进入到这个页面,那如果我们点击点赞的部分
是不是会变得高亮呀,如果我们不设置上面的方法,那么是不是点击点赞部分就会冒泡触发外面大的部分进行跳转呀,所有这个方法还是很重要的。
示例四:使用 onclick
多次绑定事件
<body>
<div id="grand" onclick="handleGrand()" onclick="handleGrand()">
<div id="parent" onclick="handleParent()">
<div id="child">
</div>
</div>
</div>
<script>
function handleGrand(){
console.log("grand1");
}
function handleGrand(){
console.log("grand2");
}
function handleParent(){
console.log("parent");
}
</script>
// 点击parent部分,打印:parent,grand2
</body>
我们只改变body
里面的代码,在这个示例中,我们尝试在 grand
元素上使用 onclick
属性多次绑定事件处理器。然而,由于 onclick
是 DOM0 的方法,只能绑定一个事件处理器,第二个 handleGrand
函数会覆盖第一个,这种方式不如使用 addEventListener
方便和灵活。
示例五:输入框的焦点和失焦事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text" name="" id="input">
<script>
let input = document.getElementById('input');
input.addEventListener('focus', function(e) {
console.log('focus');
})
input.addEventListener('blur', function(e) {
console.log('blur');
})
</script>
</body>
</html>
在这个示例中,我们为输入框绑定了焦点focus
和失焦blur
事件。这两种事件不属于事件冒泡的一部分,因此不能通过事件代理来实现。
总结
js中的事件流:
1.捕获阶段 --- 事件从window处往目标处传播
2.目标阶段 --- 在目标处触发事件
3.冒泡阶段 --- 事件从目标处往window处传播,这个就是冒泡阶段
重点:
js 中的事件默认在冒泡阶段触发
- e.stopPropagation() 阻止事件流的传播
- e.stopImmediatePropagation() 阻止事件流的传播+阻止同一个容器绑定多个相同的事件
DOM0 vs DOM2
-
DOM0:onclick... 1.无法人为修改事件在哪个阶段触发 2.只能绑定一个相同的事件
-
DOM2:addEventListener 1.可以人为修改事件在哪个阶段触发 2.同一个容器允许绑定多个相同的事件
事件代理:
把元素身上需要响应的事件,委托到另一个元素(父元素)上
无事件件流:
1.focus
2.blur
本文到此就结束了,感谢你的阅读,希望对你有所帮助!