带你深度解析Js中的事件机制
从问题谈起
最近在写一个仿饿了么的Vue
项目,实现了店铺头部部分,点击这个头部会显示店铺的基本信息:
当想要关闭时,点击下面关闭的图标发现这个页面关闭不了。为了快速发现问题,我询问了ChatGPT
,它给了我三个可能出现的问题,我一个一个去排查,发现不是前两个的问题,那就很有可能是第三个问题:
为了彻底弄懂,我查了一些资料,接下来就一起来看一下吧。
Js事件流
是什么?
Js
中事件执行的整个过程称之为事件流,分为三个阶段:事件捕获阶段,处于目标阶段、事件冒泡阶段。
事件捕获:当鼠标点击或者触发dom
事件时,浏览器会从根节点开始由外到内进行事件传播,即点击了子元素,如果父元素通过事件捕获方式注册了对应的事件的话,会先触发父元素绑定的事件。看下面图示:
处于目标阶段:传播到事件触发处,触发注册的事件。
事件冒泡阶段:与事件捕获阶段相反,由内到外进行事件传播,直到根节点。如下图所示:
有点难理解?我们直接看下面的代码演示。
代码演示
我在页面上定义了三个div
容器并设置了一定的样式(下面代码无Css
),分别拿到了他们的Dom
结构进行事件监听:
<body>
<div id="app">div1
<div id="box">div2
<div id="title">div3
</div>
</div>
</div>
<script>
let app = document.getElementById('app')
let box = document.getElementById('box')
let title = document.getElementById('title')
app.addEventListener('click', () => {
console.log('div1');
})
box.addEventListener('click', () => {
console.log('div2');
})
title.addEventListener('click', () => {
console.log('div3');
})
</script>
</body>
此时页面效果为:
当点击最里面的容器(即黑色容器)代码打印顺序为:div3
,div2
,div1
。此时用Js
事件机制解释为:当点击黑色容器时,开始进行事件捕获,Js
事件流从window
上往事件触发处传播,遇到注册的捕获事件就会触发;但是捕获阶段默认是不处理的(addEventListener
第三个参数默认是false
),紧接着传播到事件触发处,触发注册的事件,打印div3
,然后进行冒泡阶段从事件触发处 往window
上传播,遇到注册的冒泡事件会触发,打印div2
,再打印div1
。
用图示表示Js
事件流传播过程:
那我把第二个容器的监听点击事件传入第三个参数true
,不让它默认为false
即:
box.addEventListener('click', () => {
console.log('div2');
},true)
这时点击黑色容器时打印结果是什么?
我们分析一下,第三个参数为true
时,相当于第二个容器的捕获阶段会执行事件,冒泡阶段反而不会,所以打印顺序为:div2
,div3
,div1
。
阻止默认事件
上面讲到的捕获事件和冒泡事件都是默认事件,阻止默认事件的发生在我们日常开发中需要经常使用,就比如文章一开始的问题。那怎么阻止默认事件呢?
stopPropagation()
此方法可以终止默认事件传播到其他容器上。
用法
我们还是用上面代码为例,在第三个容器监听事件中加入这个方法(用事件参数调用):
title.addEventListener('click', (event) => {
console.log('div3');
event.stopPropagation()
})
同样点击黑色容器,此时打印结果只有div3
,因为这个方法阻止了事件流的传播,事件流到div3
盒子打印了div3
,接着就中断了,不会进行冒泡阶段了,用图表示为:
stopImmediatePropagation()
这个方法和上面方法相似,也是终止默认事件传播到其他容器上;同时它也可以终止自己这个容器的其他事件。
用法
还是用上面的例子,这次在第三个容器监听事件中添加两个事件,看下面代码:
title.addEventListener('click', (event) => {
console.log('div3');
event.stopImmediatePropagation()
})
title.addEventListener('click', () => {
console.log('_div3');
})
同样点击黑色容器,此时打印结果也只有div3
,这个方法终止了默认事件,同时也终止了这个容器的其他事件,如果是stopPropagation()
方法会有两个打印结果,也就是它不会终止这个容器的其他事件,这是这两个方法的区别。
事件委托
我们借助Js
存在的事件流特性能在我们开发中起大作用;如果有多个DOM
节点需要监听事件的情况下,给每个DOM
绑定监听函数,会极大的影响页面的性能,我们可以通过事件委托来进行优化,事件委托利用的就是冒泡的原理。
举个例子:如果我们在页面上5个li
都添加点击事件,你可能会这样干:
let li = document.getElementsByTagName('li')
for (let i = 0; i < li.length; i++) {
li[i].addEventListener('click', () => {
console.log(li[i].innerHTML);
})
}
这样写是没有问题的,但是我们用事件委托来完成,当点击li
的时候通过冒泡是不是在ul
上注册的事件也会触发,所以我们只需要在ul
上绑定一个点击事件,同时事件参数中有一个target
,记录是哪一个li
冒泡,可以返回触发事件的元素。这样做相当于li
上的事委托到了ul
上来完成,看下面代码:
let ul = document.getElementById('ul')
ul.addEventListener('click', (event) => {
console.log(event.target.innerText);
})
利用事件委托显然是更加优雅的,每一个函数都会占用内存空间,只需添加一个事件处理程序代理所有事件,所占用的内存空间更少。
学“废”了
看到这里,相信大家对Js
中的事件机制有了一定的了解。回到前面一开始项目中的问题,当我们点击关闭图标时,由于事件的冒泡机制,触发了关闭事件,紧接着又触发了打开事件,所以页面关闭不了。在Vue
中这个问题很好解决,我们只需要在点击事件后面添加stop
,就可以阻止冒泡, @click.stop
。本篇文章到这就结束了,创作不易,点个免费的赞是对作者最大的鼓舞ヾ(◍°∇°◍)ノ゙。
转载自:https://juejin.cn/post/7240805459287326776