对于事件的传播机制模糊不清的同学,可以看一下这篇文章。

一、事件、事件监听函数、事件对象

首先,我们明确一下事件、事件监听函数、事件对象,这三个概念之间的关系。

(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上,而不影响点击其他区域。

 

你还知道哪些对于捕获和冒泡的应用,告诉我吧。

本人水平非常有限,写作主要是为了把自己学过的东西捋清楚。如有错误,还请指正,感激不尽。