彻底弄明白事件的捕获和冒泡
对于事件的传播机制模糊不清的同学,可以看一下这篇文章。
一、事件、事件监听函数、事件对象
首先,我们明确一下事件、事件监听函数、事件对象,这三个概念之间的关系。
(1)事件,就是发生了什么事,比如点击事件、文本框输入事件、ajax成功返回事件等。
(2)事件监听函数,是在事件发生时我们想执行的代码,是我们在事件上挂的钩子。
(3)事件对象,是对事件的描述对象,就是我们熟悉的那个“e”。每个事件发生时,事件对象都会作为一个参数,被传入事件监听函数。浏览器原生提供一个Event对象,每个事件对象,都是Event的一个实例。并且除了事件发生时自动生成的那个“e”,我们还可以通过 new Event(type, options)
,来自己生成一个事件对象。
二、事件的捕获和冒泡
你可能会奇怪,为什么会有捕获和冒泡这种东西,搞得这么复杂?
当你点击页面上的一个按钮,你可以说你点击了按钮,但也可以说,你点击了包着它的div,你点击了这个页面,甚至你点击了window,你都挑不出毛病来,因为确实如此。那怎么办呢?
浏览器设计了这样一个事件模型:当你点击一个按钮时,事件从最外层的window,一层层的往下传播,直到这个按钮,这个过程称为捕获。然后再回去,从这个按钮开始,一层层的往上传播,直到最外层的window,这个过程被称为冒泡。
捕获阶段,从window开始,到目标节点结束。冒泡阶段,从目标节点开始,到window结束。
比如我们点击了div中的一个span标签。事件在页面上是这么传播的:
页面上的任何一个点击,都会以这样一个“U型”在页面上传播一遍。除非你阻止了传播,或者这个事件本身不冒泡。(这个我们后面再谈)
事件监听函数,就是我们在这个路径上挂的钩子。你在路径的哪个位置挂钩子,事件传播到这儿时,就会触发监听函数。
三、事件对象的bubbles属性
事件对象,有一个bubbles属性,意思是该事件是否冒泡。
这是什么意思呢?因为不是所有事件都会冒泡的。有些会冒泡,比如click、mouseover,有些是不冒泡的,比如blur、mouseenter。我们可以通过 e.bubbles 来看这个事件是否冒泡。
new Event(type, options) 的第二个参数,也可以配置bubbles这个属性,不传的话默认值是false。默认值为false的意思是说,用构造函数自定义的事件对象,默认是一个不冒泡的事件。
而我们知道,addEventListener的第三个参数,false是冒泡,true是捕获。它的意思其实是,把这个钩子挂在捕获阶段还是冒泡阶段。
这儿我就产生了疑问,如果bubbles属性也是这么个逻辑,一个事件要么冒泡要么捕获。那为什么,我们在捕获和冒泡阶段挂的监听函数,都会执行呢?
有疑问,就自己试验一下:
<div><span>哈哈哈哈哈哈哈哈</span></div> <script> let div = document.querySelector('div'); let span = document.querySelector('span'); div.addEventListener('click', function(e) { console.log('div冒泡'); }, false); div.addEventListener('click', function(e) { console.log('div捕获'); }, true); let e = new Event('click', { bubbles: false }); span.dispatchEvent(e); // 我们可以自己触发事件监听函数,就像jquery那样。这个函数需要的参数,就是一个Event对象。我想这也就是Event构造函数的主要用处。 // 输出结果: // div捕获 </script>
当bubbules为false,就是说不冒泡时,只有捕获阶段的钩子被触发了。
然后我把bubbles改为true,再试一次。
<div><span>哈哈哈哈哈哈哈哈</span></div>
<script>
let div = document.querySelector('div');
let span = document.querySelector('span');
div.addEventListener('click', function(e) {
console.log('div冒泡');
}, false);
div.addEventListener('click', function(e) {
console.log('div捕获');
}, true);
let e = new Event('click', { bubbles: true });
span.dispatchEvent(e);
// 输出结果:
// div捕获
// div冒泡
</script>
捕获和冒泡阶段的钩子,都被触发了。
就是说,bubbles属性的逻辑是:false时事件只捕获不传播,true时事件同时捕获和传播。这下就解释通了,为什么我们平时捕获和冒泡阶段挂的钩子都能触发了。
四、事件的阶段
事件对象还有一个属性:eventPhase,表示该监听函数被触发时,事件正处在传播过程的哪个阶段。这个值从0到3,分别指事件没有发生、捕获阶段、目标阶段、冒泡阶段。
在捕获阶段和冒泡阶段中间,还有一个目标阶段。事件传播的过程,来的路上都是捕获阶段,到达了目标节点是目标阶段,去的路上都是冒泡阶段。
<div><span>哈哈哈哈哈哈哈哈</span></div>
<script>
let div = document.querySelector('div');
let span = document.querySelector('span');
let jieduan = ['没有发生', '捕获阶段', '目标阶段', '冒泡阶段'];
div.addEventListener('click', function(e) {
console.log(e.currentTarget.tagName, jieduan[e.eventPhase]);
}, false);
div.addEventListener('click', function(e) {
console.log(e.currentTarget.tagName, jieduan[e.eventPhase]);
}, true);
span.addEventListener('click', function(e) {
console.log(e.currentTarget.tagName, jieduan[e.eventPhase]);
}, false);
span.addEventListener('click', function(e) {
console.log(e.currentTarget.tagName, jieduan[e.eventPhase]);
}, true);
// 输出结果:
// DIV 捕获阶段
// SPAN 目标阶段
// SPAN 目标阶段
// DIV 冒泡阶段
</script>
浏览器总是把最里面、最具体的那个元素,作为事件的目标元素。是否处于目标阶段,可以用来判断,事件是否是元素自身触发的,而不是来自其内部节点的冒泡或捕获。
判断事件是否由元素自身触发,更常用的是:e.currentTarget==e.target。
五、阻止事件的传播
在事件传播U型的任何一个钩子上,我们都可以阻止事件的继续传播,只需要调用:e.stopPropagation(),事件就不会再继续传播。
比如我们执行以下代码:
window.addEventListener('click', function(e) {
e.stopPropagation();
}, true);
那么这个页面上所有的点击事件监听函数,都被屏蔽掉了。
注意,屏蔽的只是挂载的事件监听函数,而不是事件的默认行为。比如你点击一个链接,还是会跳转的。
六、我们如何应用?
(1)对捕获和冒泡最常见的应用,是通过 e.currentTarget==e.target 或者 e.eventPhase==2 判断事件的触发源是不是元素自身。vue的.self事件修饰符,也是这个原理。
比如我们可以用这个,点击空白处关闭页面蒙层。
(2)有些页面较为复杂,无法通过简单的判断触发源是否是元素自身,来满足我们对点击位置的要求。
比如有个场景是:除了页面上的两处位置,点击页面其他位置,就执行一个函数。
这个时候你可以在这两个元素的捕获阶段钩子上,阻止点击的继续传播。然后在window的点击事件上,执行你想要的操作。这样点击这两个元素时,事件就不会冒泡到window上,而不影响点击其他区域。
(3)事件代理。比如一个列表,因为点击事件会冒泡,我们可以把原先在每个<li>上绑定事件,改为只在<ul>上绑定。这样减少了很多页面的事件绑定,可以很好的优化页面性能。
你还知道哪些对于捕获和冒泡的应用,告诉我吧。
本人水平非常有限,写作主要是为了把自己学过的东西捋清楚。如有错误,还请指正,感激不尽。