- 作者:老汪软件技巧
- 发表时间:2024-12-27 10:06
- 浏览量:
今天我们来聊聊js中的事件流机制,以及借助事件流机制实现的事件代理。
1. 事件流机制
什么是事件流呢?我们直接从代码切入,来理解一下事件流。
html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<style>
.red {
width: 400px;
height: 400px;
background-color: red;
}
.green {
width: 300px;
height: 300px;
background-color: green;
}
.blue {
width: 200px;
height: 200px;
background-color: blue;
}
style>
head>
<body>
<div class="red">
<div class="green">
<div class="blue">div>
div>
div>
<script>
let red = document.querySelector('.red')
let green = document.querySelector('.green')
let blue = document.querySelector('.blue')
red.addEventListener('click', () => {
console.log('red');
})
green.addEventListener('click', () => {
console.log('green');
})
blue.addEventListener('click', () => {
console.log('blue');
})
script>
body>
html>
我在页面上放了三个容器,red、green和blue,它们的宽和高各不相同,并且blue是嵌套在green里的,green又是嵌套在red里的。
然后我用js获取到3个容器,分别给它们绑定一个点击事件。当点击red时输出red,点击green时输出green,点击blue时输出blue。
现在页面上长这样合情合理吧。
那我们现在点击红色容器应该会输出red:
确实输出了red。那我现在点击绿色容器输出green也合情合理吧。
你发现,哎,怎么输出了green和red啊?这算怎么一回事呢?我明明只点了green容器啊。
我再点击蓝色容器看看:
我们发现点击了蓝色容器,blue、green和red都输出了。
你说这合理吗?你这么想一想,绿色容器是不是红色容器的一部分,那我点了绿色容器算不算也点到了红色容器?蓝色容器是不是绿色容器和红色容器的一部分,那我点到了蓝色容器算不算点到了绿色容器和红色容器?
这么一想好像确实挺合理的,那我们再来看看输出结果。
当我们点击蓝色容器时它的输出结果是先输出blue再输出green再输出red。你说当我们点击蓝色容器的时候,是先点击到红色容器还是先点击到蓝色容器呢?
没有红色容器哪里的蓝色容器?红色容器是蓝色容器的父元素吧。所以当我们点到蓝色容器时,一定是先点击到了红色容器然后才点击到了蓝色容器,但我们的输出结果却不是先输出red,而是先输出blue。这是为什么呢?
想要搞清楚这个,那就得聊到我们今天的主题了——事件流机制。
事件流机制有3个阶段。第一个阶段是捕获阶段,指的是事件行为从 window上 向目标元素传播。
什么意思呢?你说当我点到了蓝色容器,除了点到了红色容器,是不是还点到了body容器,是不是还点到了html容器,是不是还点击到了整个window容器?所以当我们点击蓝色容器时,这个事件行为一定是先出现在window容器身上,然后层层向内部传播,跨过html容器,跨过body容器,紧接着跨过红色容器跨过蓝色容器最后落到蓝色容器身上。这就叫做事件行为从 window上 向目标元素传播。
事件行为跨越层层障碍最后落到它的目标元素——蓝色容器身上时,就会执行第二个阶段,目标阶段。在这个阶段,事件行为就会在目标元素上触发。此时蓝色容器身上的事件触发,于是输出blue。
目标阶段执行完毕之后,就会执行第三个阶段——冒泡阶段。这个阶段事件行为从目标元素上向 window 上传播。意思就是事件行为找到了它的目标元素并触发了它身上的函数之后,就会往回走,经过绿色容器并将它身上的函数触发,经过红色容器并把它身上的函数触发,然后一路走到window容器身上终止。
所以你能理解为什么我们点击蓝色容器输出结果会是先输出blue再输出green再输出red了吧。当我们点击蓝色容器时,这个事件行为就会一路从window出发走到蓝色容器身上,触发掉蓝色容器身上的函数之后紧接着返回,然后将返回路上容器的函数也触发掉了。如果我们在body容器身上也绑定一个事件的话,它就会在红色容器的函数执行完后触发,你能理解为什么吧。
所以这就是事件流机制:
捕获阶段 -- 事件行为从 window上 向目标元素传播目标阶段 -- 事件行为在目标元素上触发冒泡阶段 -- 事件行为从目标元素上向 window 上传播
所以js 中的事件离开目标处后默认都是在冒泡阶段触发的。
2. onclick 和 addEventListener 的区别
以上就是事件流机制,还是挺简单易懂的吧。但你可能会有一个疑问,既然都有捕获阶段了,为什么我们不能让函数就在捕获阶段触发呢?如果函数在捕获阶段触发的话,那当我们点击蓝色容器时,输出结果应该就是red、green和blue了吧。
那我们可以人为地控制函数在捕获阶段触发吗?是可以的,那我们就可以借此来聊一聊onclick 和 addEventListener 的区别了。我们知道这两个函数都可以绑定事件,那它们有什么不同之处呢?
我们一般使用addEventListener函数时只会传两个参数,第一个是事件的类型,第二个是一个回调函数。其实它还能接收第三个参数。
它的第三个参数默认是false,如果我们加上它,不会有任何变化,但如果我们将它改为true,那就会有变化了,你来看:
<script>
let red = document.querySelector('.red')
let green = document.querySelector('.green')
let blue = document.querySelector('.blue')
red.addEventListener('click', () => {
console.log('red');
}, true)
green.addEventListener('click', () => {
console.log('green');
}, true)
blue.addEventListener('click', () => {
console.log('blue');
})
script>
我把前两个事件的第三个参数改为了true,此时我再点击蓝色容器,你来看看现在的输出结果:
我们发现就是先red再green再blue了,所以你应该明白了,addEventListener的第三个参数是用来控制事件是在捕获阶段触发还是在冒泡阶段触发的。当为false时,就是默认状态,在冒泡阶段触发;而当我们人为的改为true时,它就会在捕获阶段触发了。
那这就是onclick 和 addEventListener 的第一个区别了,onclick 事件只能在冒泡阶段触发且不能人为控制,而 addEventListener事件可以人为控制事件在捕获或者冒泡阶段触发。
它们还有第二个区别,就是onclick 只能同时绑定一个相同的事件,addEventListener 可以绑定多个。
你来看:
<script>
let red = document.querySelector('.red')
let green = document.querySelector('.green')
let blue = document.querySelector('.blue')
red.onclick = function () {
console.log('red');
}
red.onclick = function () {
console.log('red2');
}
script>
现在我用onclick只给红色容器绑定了点击事件,并且我给它绑定了两个相同的事件。现在当我点击红色容器时,我们来看看输出结果:
输出结果只有red2了,说明第二个事件将第一个事件覆盖掉了。而我再用addEventListener做同样的操作:
<script>
let red = document.querySelector('.red')
let green = document.querySelector('.green')
let blue = document.querySelector('.blue')
red.addEventListener('click', () => {
console.log('red');
})
red.addEventListener('click', () => {
console.log('red2');
})
script>
我们再来看看输出结果:
我们发现red和red2都输出了。所以onclick不能同时绑定多个相同的事件。
onclick 和 addEventListener 的区别
onclick 事件在冒泡阶段触发且不能人为控制onclick 只能同时绑定一个相同的事件 addEventListener 可以绑定多个3. 阻止事件流的传播
我们再来聊聊怎么阻止事件流的传播。
为什么要阻止事件流的传播呢?你想一想,事件流的机制好吗?当我点击蓝色容器时,会触发蓝色容器身上的事件,这合情合理,一点毛病没有。但因为你事件流的传播,帮我把绿色容器和红色容器的事件也触发了,我需要你帮我把绿色容器和红色容器的事件触发吗?我点了蓝色容器,肯定是希望只想触发蓝色容器的事件啊,如果我想触发绿色容器和红色容器我直接去点你不就好了。
再比如,掘金的文章模块:
点赞模块是文章模块中的一个子容器没错吧。当我点击文章模块就会跳转到对应的页面中去,它身上肯定绑定了事件;当我点击点赞模块点赞数就会加1,它身上肯定也绑定了事件。那你说,当我点击点赞模块会跳转到文章页面吗?不会的吧,我们点一下就知道了,点击点赞模块只会点赞数量加1,不会跳转页面。但按理来说,点赞模块是文章模块的子元素。当点赞事件触发了,就会来到冒泡阶段,那文章模块上的事件也会触发才对。所以,在这里,冒泡阶段肯定是被除掉了,所以文章模块的事件才没有触发。
这就是为什么要阻止事件流的传播,人家用户只是想给文章点个赞而已,你不能让它跳转页面吧。
所以我们来看看哪些方法能阻止事件流的传播。
第一个能阻止事件流传播的方法:e.stopPropagation()。
我们知道,绑定了事件的函数一定会有一个事件参数的,这个事件参数我们一般取e,而事件参数上就有一个函数stopPropagation,它能阻止事件流的传播。
所以我们把它写在blue的事件里。当事件流传播到目标阶段触发了目标的事件之后,我们就不让事件流继续传播了,那冒泡阶段就不复存在了。
<script>
let red = document.querySelector('.red')
let green = document.querySelector('.green')
let blue = document.querySelector('.blue')
red.addEventListener('click', () => {
console.log('red');
})
green.addEventListener('click', () => {
console.log('green');
})
blue.addEventListener('click', (e) => {
console.log('blue');
e.stopPropagation()
})
script>
现在当我点击蓝色容器时:
你看,就只输出了blue,目标阶段触发了但冒泡阶段没有触发。
既然stopPropagation能阻止事件流的传播,它可以砍掉冒泡阶段,那它能不能砍掉捕获阶段呢?
我们来试一下:
<script>
let red = document.querySelector('.red')
let green = document.querySelector('.green')
let blue = document.querySelector('.blue')
red.addEventListener('click', (e) => {
e.stopPropagation()
console.log('red');
}, true)
green.addEventListener('click', () => {
console.log('green');
}, true)
blue.addEventListener('click', (e) => {
console.log('blue');
})
script>
我把red和green事件的第三个参数改为了true,这两个事件就会在捕获阶段触发。我还把e.stopPropagation()写在了red事件里面。既然你能阻止事件流的传播,那捕获阶段应该也能阻止才对,所以当red事件触发了之后后面事件流就不再传播了,后面的事件也不会执行。
现在我点击蓝色容器,red事件就会在捕获阶段触发,那它后面的事件会不会触发呢?
只输出了red,说明没有触发。捕获阶段就被阻止了。
所以e.stopPropagation()能阻止事件流的传播,不管是在冒泡阶段还是在捕获阶段。
还有第二个能阻止事件流传播的方法,e.stopImmediatePropagation()。
<script>
let red = document.querySelector('.red')
let green = document.querySelector('.green')
let blue = document.querySelector('.blue')
red.addEventListener('click', (e) => {
console.log('red');
})
green.addEventListener('click', () => {
console.log('green');
})
blue.addEventListener('click', (e) => {
console.log('blue');
e.stopImmediatePropagation()
})
script>
我把它写在了blue事件里,当我点击blue容器时,冒泡阶段应该会消失。
只输出了blue。
所以e.stopPropagation()和e.stopImmediatePropagation()都能阻止事件流的传播,那它们有什么不同之处呢?
我们来看:
<script>
let red = document.querySelector('.red')
let green = document.querySelector('.green')
let blue = document.querySelector('.blue')
red.addEventListener('click', (e) => {
console.log('red');
e.stopPropagation()
}, true)
red.addEventListener('click', (e) => {
console.log('red2');
}, true)
script>
我用addEventListener给红色容器绑定了两个相同的事件,一个输出red,一个输出red2,并将它们的第三个参数改为了true,我还在第一个点击事件了加上了e.stopPropagation()。这样当我点击蓝色容器时,它们就会在捕获阶段触发。
red和red2都输出了。没问题,我再将e.stopPropagation()改为e.stopImmediatePropagation(),我再点击蓝色容器:
此时就只输出了red。说明了什么?说明e.stopImmediatePropagation()能阻止同一个dom结构下其它相同事件的触发。
这就是e.stopPropagation()和e.stopImmediatePropagation()区别:
它们都能阻止事件流的传播,而e.stopImmediatePropagation()还能阻止同一个dom结构下其它相同事件的触发,e.stopPropagation()不能。
4. 事件代理
最后我们来聊聊事件代理。
既然有事件流的传播机制存在,当我们点击子元素时,父元素身上的事件也会在冒泡阶段触发。那我们能不能用它来做点什么。
我们来完成一个简单的需求。
我在页面上放一个ul,里面有5个li,每个li的值从1开始递增。现在我想点到哪个li就输出哪个li的值。
我们可以这样写吧:
<body>
<ul>
<li>1li>
<li>2li>
<li>3li>
<li>4li>
<li>5li>
ul>
<script>
let li = document.querySelectorAll('li')
li.forEach((item, i, arr) => {
item.addEventListener('click', () => {
console.log(item.innerText);
})
script>
body>
使用querySelectorAll函数获取到页面上所有的li,会存放到一个类数组里,我们再去遍历这个类数组,给每一个li添加一个点击事件。这样就完成了。
这样我们就给每个li添加了一个事件,直接在目标身上进行操作。
那我们能不能利用事件流的传播机制来写。我们可不可以在li的父元素ul身上绑定点击事件,当我们点到li时,ul身上的事件是会在冒泡阶段触发的
<body>
<ul>
<li>1li>
<li>2li>
<li>3li>
<li>4li>
<li>5li>
ul>
<script>
let ul = document.querySelector('ul')
ul.addEventListener('click', function () {
})
script>
body>
既然我们是在ul身上绑定点击事件,当它在冒泡阶段触发的时候,我们怎么去输出li的值呢?
这就要用到事件参数了,事件参数会记录这一次事件的详细参数,当然也会记着是从哪一个li触发的事件的。我来输出给你看看,假如我点击了第三个li:
在这个事件参数的target属性中,记录着这样一个信息:
所以我们可以利用这一点,我们直接输出e.target.innerText就行了。
<body>
<ul>
<li>1li>
<li>2li>
<li>3li>
<li>4li>
<li>5li>
ul>
<script>
let ul = document.querySelector('ul')
ul.addEventListener('click', function (e) {
console.log(e.target.innerText);
})
script>
body>
这样当我们点击ul中的li时,ul身上的事件就会在冒泡阶段触发,然后我们再去输出点击的是哪个li。
这样同样能实现第一种方法的效果。这就是事件代理,借助事件的冒泡机制,将原本应该批量绑定在子容器上的事件绑定在父元素上,然后通过事件对象的target来判断事件源。
这样写的好处是什么。假如我们ul中有100个li,那我们用第一种方法就要循环遍历100次给每个li都绑定一个点击事件,既增加了时间复杂度又增加了空间复杂度。而我们通过事件代理,将事件绑定在ul身上,只要写一个函数就能完成。这就是事件代理的好处。
但不是所有的事件都能进行事件代理,input 标签上的focus(聚焦)和blur(失去焦点)事件无法被代理。意思就是如果我们准备一个父容器包裹一个input标签,如果我们给父容器绑定focus或blur事件时,当我们对input框进行操作,父容器身上的事件是不会触发的,事件代理就不起效果。我们在使用事件代理的时候注意一下这两个就行。
5. 总结
本次我们一起来学习了一下js的事件流机制,它分为3个步骤:捕获阶段、目标阶段和冒泡阶段。还讲了一下onclick 和 addEventListener 的区别以及阻止事件流传播的方法,最后我们讲了一下事件代理,是利用事件流传播机制的一种巧妙实现。
如果对你有帮助的话请点个赞吧!