前言
在 JavaScript 的世界里,事件流就像一只永不停歇的蝴蝶,每一个动作、每一个点击、每一个滚动,都会触发一连串的蝴蝶效应。作为一名开发者,掌握事件流的艺术,不仅能让你的网页更加生动、更加交互,也能让用户体验到前所未有的舒适。那么,事件流到底是什么?它又是如何影响我们对网页的设计和开发?接下来让我们来向这只蝴蝶学习一下。
1. 事件流
1.1 事件流是什么
JavaScript 事件流(Event Flow)指的是当用户与网页进行交互时(如点击、鼠标移动、键盘输入等),浏览器如何处理这些事件的顺序和方式。它描述了事件从页面最顶层元素(文档)开始,如何层层传递到目标元素,然后又如何回溯到顶层元素的过程。
正所谓百闻不如一见,光说大家可能看起来有点懵懵的,接下来让我用一段代码给大家展示一下事件流的效果:
html
<!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>
.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>
上述代码我们创建了三个div盒子分别是红、绿、蓝并且把它们按照这个顺序给嵌套在了一起还在每个盒子上面添加了点击事件,每个盒子的点击事件会分别打印对应颜色的英文,接下来我们可以试试如果点击最里面那层blue会发生什么效果:
tips:注意它们嵌套顺序
当看到这个结果时大家可能并不会感到疑惑,因为从我们的视觉角度来看它们是层层叠加在一起的,如果点击蓝色的话同时也相当于点击了下面的绿色和红色所以它们会按照我们所看到的页面叠加顺序依次触发。
但是有些同学考虑到了嵌套顺序的话可能会有点疑惑,明明红色的是在最外层,为什么触发的时候不是按照代码中的嵌套顺序触发,而是先触发最里面的那个blue呢?这就不得不提到事件流的三个阶段了,那些对上面效果产生疑惑的同学到下一小节会得到答案。
1.2 事件流的三个阶段
说起事件流,顾名思义,它肯定不是一个事件,而是由很多个阶段所组成的,事件流的组成包括以下三个阶段:捕获阶段 、目标阶段 、冒泡阶段,并且执行一次事件时这三个阶段是依次执行的。那么这三个阶段是什么意思呢,下面我们先来给大家解释一下,大家就能对上一小节的效果有个大概的了解了:
- 捕获阶段 -- 事件行为从 window 上向目标元素传播
- 目标阶段 -- 事件行为在目标元素上触发(最里面的目标都是在目标阶段触发)
- 冒泡阶段 -- 事件行为从目标元素上向 window 上传播
上一小节代码执行时不是我们肉眼所观察到的那么简单的,接下来我们为大家解释一下上述代码在执行的过程中到底是怎么样的。
首先当我们点击页面最上方的蓝色盒子时,我们要触发它的点击事件,此时我们会进行捕获阶段。在html中我们将其嵌套在了最内层的div中,所以我们将会从最外层的red盒子开始依次向内进行查找,直到找到最内层的blue盒子为止,过程如下图所示:
接下来当我们找到目标元素后我们就会让事件行为在目标元素blue盒子上触发,此时控制台会打印blue,接下来我们就会进行冒泡阶段进行回溯,从最内层向最外层依次执行每个盒子的事件,过程如下:
这一整个完成的过程就是我们所说的事件流的执行过程,我们身为程序员肯定不能这么死板的去记东西,如果上面的比较官方的表述看不懂接下来我们可以想象自己是魂殿长老,没吃过猪肉还没见过猪跑嘛。
这个事件流就像是我们抓到了主角,然后想要对主角的灵魂深处种下印记从而控制他,这时候我们得从外面一直向内捕获,直到捕获到他的灵魂最深处,也就是抓到目标了嘛,这就是 捕获阶段 。
接下来我们就会对他的灵魂中种下印记,后来有一天我们察觉到这个主角贼心不死,然后我们就想从内引爆他的那个灵魂印记,这就是我们所说的目标阶段。
恰巧呢我们又是天选之人,这个主角没反应过来,然后灵魂印记就会由内而外依次引从而让他身死道消,这就是我们所说的冒泡阶段。
到这里了你就已经是一名合格的魂殿长老了,关于事件流呢还有个小tips:
js 中的事件离开目标处后,默认都是在冒泡阶段触发的
2. onclick 和 addEventListener 的区别
在js中我们都知道,在旧版本的js中是用onclick
来进行点击事件触发的,而新版本中添加了addEventListener
来进行触发的,正所谓官方不会闲的没事干就为了让代码高级一点整的那么长一串,接下来我们就来讲讲两者的区别。
2.1. onclick 事件在冒泡阶段触发,且不能人为控制
这一点呢我们从上面一个小结的末尾那个tip可以看出一点端倪,什么叫默认都是在冒泡阶段触发的?关于这两种不同的事件触发的方式,虽然它们两种方式都是默认从冒泡阶段开始触发的,但是在新版本中的addEventListener
我们可以对其进行人为控制,因为addEventListener
中存在第三个参数就是用来控制事件从哪个阶段开始触发的:
addEventListener第三个参数:
false
是冒泡阶段触发,true
是捕获阶段触发
接下来我们还是继续用前面那个例子,我们将addEventListener中加入第三个参数,并且将其更改成true。:
tip:为了方便只展示js其余和开头代码一样
js
<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');
}, true)
</script>
接下来我们再来试试点击blue会打印出来什么:
我们根据控制台的打印可以发现,它的打印顺序变成了根据盒子嵌套的顺序进行了打印,而这个顺序正好是捕获阶段捕获div的顺序,这就是addEventListener中第三个参数的作用,让事件在捕获阶段触发。
2.2. onclick 只能同时绑定一个相同的事件,addEventListener 可以绑定多个相同事件
看到这个小标题大家可能会有点疑惑,什么叫绑定相同的事件呢?这个意思是对同一个dom元素绑定相同的事件,接下来我们对red给它绑定两个相同的click事件,每次打印结果不一样,接下来只展示js部分,其余部分同上:
js
let red = document.querySelector('.red')
let green = document.querySelector('.green')
let blue = document.querySelector('.blue')
red.onclick = () => {
console.log('red');
}
red.onclick = () => {
console.log('red2');
}
接下来我们看看点击red盒子会发生什么:
根据上面的结果我们可以发现是用onclick绑定两个相同事件的话,如果事件触发,那么后面的会把前面的给覆盖了,所以它只能同时绑定一个相同事件,接下来我们来看看addEventListener:
js
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');
})
接下来我们看看它与onclick的区别:
我们可以发现当我们为red绑定两个相同事件的时候,如果点击red,那么这两个事件在默认条件下会先后进行触发,这就是它和onclick的区别。
onclick 和 addEventListener 的区别:
- onclick 事件在冒泡阶段触发,且不能人为控制
- onclick 只能同时绑定一个相同的事件,addEventListener 可以绑定多个相同事件
3. 阻止事件流的传播
大家都是文化人,有句古话叫 "抽刀断水水更流",关于事件流这条小河我们可以一刀直接咔一下把它给终止传播了,直接就是独断万古。阻止事件流的传播主要有两种方式分别是:e.stopPropagation()
和e.stopImmediatePropagation()
,接下来我们就来了一下这两个方法的使用以及它们的区别。
tips:e是事件参数,在接下来的小结会解释
3.1 e.stopPropagation()
关于这两个方法的使用其实都是大差不差的,我们只需要在需要进行中断的地方加上这个方法的调用就行,接下来用代码给大家演示一下,还是之前的那三个盒子来做示范,只展示js。我们让这三个事件从捕获阶段就开始触发:
js
<script>
let red = document.querySelector('.red')
let green = document.querySelector('.green')
let blue = document.querySelector('.blue')
red.addEventListener('click', () => {
console.log('red');
e.stopPropagation()
}, true)
green.addEventListener('click', () => {
console.log('green');
}, true)
blue.addEventListener('click', () => {
console.log('blue');
}, true)
</script>
根据上面的结果我们可以看到,当我们在console.log('red');
后放置了e.stopPropagation()
并进行点击之后。后面的代码就不执行了,此时事件流的传播就被中断了,所以我们可以得出它的使用方法:想要在哪里进行事件流的中断,就把它放在那条语句的后面。
但是它虽然能阻止别人,但是阻止不了自己身上相同事件的触发,正所谓每个人都有心魔嘛,心魔都比较难以战胜,接下来给大家展示一下:
js
<script>
let red = document.querySelector('.red')
let green = document.querySelector('.green')
let blue = document.querySelector('.blue')
red.addEventListener('click', () => {
console.log('red');
e.stopPropagation()
}, true)
red.addEventListener('click', () => {
console.log('red2');
}, true)
green.addEventListener('click', () => {
console.log('green');
}, true)
blue.addEventListener('click', () => {
console.log('blue');
}, true)
</script>
我们可以看到,当定义了两个相同事件的时候,e.stopPropagation()
并不能阻止它身上另一个相同事件的发生,这点得注意一下。
3.2 e.stopImmediatePropagation()
这个方法跟上面的方法的使用方法一样,只不过它长有长的好处,e.stopPropagation()
干不了的事我e.stopImmediatePropagation()
能干,它可以阻止同一个 dom 结构下其他相同事件的触发,接下来我们来看看效果:
js
<script>
let red = document.querySelector('.red')
let green = document.querySelector('.green')
let blue = document.querySelector('.blue')
red.addEventListener('click', () => {
console.log('red');
e.stopImmediatePropagation()
}, true)
red.addEventListener('click', () => {
console.log('red2');
}, true)
green.addEventListener('click', () => {
console.log('green');
}, true)
blue.addEventListener('click', () => {
console.log('blue');
}, true)
</script>
我们可以看red的点击事件中后面打印red2并没有触发,只触发了前面这个事件,所以这个方法不仅可以阻止事件流的传播,还可以阻止同一个dom结构上相同事件的触发。
阻止事件流的传播
- e.stopPropagation()
- e.stopImmediatePropagation() --- 额外:阻止同一个 dom 结构下其他相同事件的触发
4. 事件委托 / 事件代理
这个事件委托呢,顾名思义,就是将事件委托给另一个来处理,下面我们先看看官方的解释:
事件委托
借助事件的冒泡机制,将原本应该批量绑定在子容器上的事件绑定在父元素上,然后通过事件对象 target 来判断事件源
在上一小节我们已经见识到了e上面的两个阻止事件流传播的方法,接下来我们看看它其中的target
。在这里可能就会有同学问了e
到底是个什么东西呢,接下来我们到控制台来打印一下来看看e到底是什么,我们在页面中放置了一个ul列表其中有5个li:
html
<!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>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
let ul = document.querySelector('ul')
ul.addEventListener('click', function(e) {
console.log(e);
})
</script>
</body>
</html>
我们根据浏览器的打印结果可以得知e就是本次事件的所有详细信息,而其中的target就是我们所点击的那个dom结构,所以根据这一点我们可以实现一个功能:现在我想要点击ul中的随意一个li,控制台能根据我所点击的那个li打印出它li中的内容。这个功能说起来很简单,虽然说我们可以直接加五个addEventListener
,但是有点不太优雅,这时候我们就可以用事件参数e
来实现这个功能,接下来看代码实现,只展示js,其余和上面一样:
js
<script>
let ul = document.querySelector('ul')
ul.addEventListener('click', function(e) {
console.log(e.target.innerText);
})
</script>
我们可以看到根据事件参数的特性可以很好的实现这一个功能,但是事件委托并不是什么事件都有的得注意一点:
input中的focus事件和blur事件不会进行事件代理
接下来我们看一段代码示例:
html
<!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" id="input">
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
let ul = document.querySelector('ul')
ul.addEventListener('click', function(e) {
console.log(e.target.innerText);
})
let input = document.querySelector('#input')
input.addEventListener('input', function() {
console.log(this.value);
})// input中的focus事件和blur事件不会进行事件代理
document.body.addEventListener('focus', function(e) {
console.log(e.target.value);
})
</script>
</body>
</html>
我们可以看到当我们进行focus事件时,并没有打印出e.target.value,而这一点在我们使用过程中需要注意。
结语
就像蝴蝶扇动翅膀可以引起远方的风暴一样,JavaScript 事件流的每一个细节都可能对我们的网页应用产生深远的影响。通过深入理解和掌握事件流的机制,我们可以创造出更加流畅、更加交互的用户体验。
最后感谢各位大佬们的观看!