关于浏览器里事件的捕获和冒泡及监听器执行的顺序

本文并不是一篇实用的文字,不考虑兼容性,而在于机制的理解。
关于本文的题目,不叫“js事件的捕获和冒泡”,是因为码者并不清楚这种叫法准不准确,于是用一个不那么精确的“浏览器”一词。
测试环境:Firefox Quantum 61.0.2 (64 位)

发现问题(场景)

下面n段代码的输出?

(代码一)基本的嵌套

<div onclick="outer()">
	<div onclick="middle()">
		<div onclick="inner()">gogo</div>
	</div>
</div>

function outer(){
	console.log('outer');
}
function middle(){
	console.log('middle');
}
function inner(){
	console.log('inner');
}

结果

inner
middle
outer

思考:看起来,内层dom的事件监听函数先执行(不过,过早的下结论是很不明智的)。也就是当父子标签都有事件注册的时候,点击子组件 => 父子标签的监听器都会执行(当然,点击父组件的其他区域,子组件的监听器不会执行)。但是,像css一样,子标签自己的东西(比如font-size),优先级高一点。

(代码二)换一种事件注册方式: addEventListener()

<div id="outer">
	<div id="middle">
		<div id="inner">gogo</div>
	</div>
</div>

// 获取dom
function get(id){
	return document.getElementById(id);
}
get('outer').addEventListener('click',function(e){
	console.log('outer');
});
get('middle').addEventListener('click',function(e){
	console.log('middle');
});
get('inner').addEventListener('click',function(e){
	console.log('inner');
});

结果

inner
middle
outer

思考: 这里看起来没什么区别,但是,其实addEventListener()的参数不止两个。

(代码三)addEventListener() 的第三个参数

第三个参数的默认值是false,这里我们先观察一下值为true的情况。

<div id="outer">
	<div id="middle">
		<div id="inner">gogo</div>
	</div>
</div>

function get(id){
	return document.getElementById(id);
}
get('outer').addEventListener('click',function(e){
	console.log('outer');
},true);
get('middle').addEventListener('click',function(e){
	console.log('middle');
},true);
get('inner').addEventListener('click',function(e){
	console.log('inner');
},true);

结果

outer
middle
inner

思考: 执行顺序从由内到外,变成从外到内了。先不要去考虑其原理。从应用的角度来说,如果业务逻辑需要先执行外层监听器,后执行内层监听器,那么,addEventListener()很合适。

(代码四)混合一下

addEventListener() 第三个参数既有false又有true会怎样?

<div id="outer">
	<div id="middle">
		<div id="inner">gogo</div>
	</div>
</div>

// 三个参数
get('outer').addEventListener('click',function(e){
	console.log('outer');
},true);
get('middle').addEventListener('click',function(e){
	console.log('middle');
},true);
get('inner').addEventListener('click',function(e){
	console.log('inner');
},true);
// 两个参数(或者第三个参数为false)
get('outer').addEventListener('click',function(e){
	console.log('outer,false');
});
get('middle').addEventListener('click',function(e){
	console.log('middle,false');
});
get('inner').addEventListener('click',function(e){
	console.log('inner,false');
});

结果

outer
middle
inner
inner,false
middle,false
outer,false

思考: 这里有点乱,面临“乱”,可以从原理的角度思考这个问题,先把这个乱放在一边,之后再回来看看这段代码。

事件的捕获和冒泡

所以,这里才是正文的开始[偷笑.jpg]

<div onclick="outer()">
	<div onclick="middle()">
		<div onclick="inner()">gogo</div>
	</div>
</div>
从鼠标点击“gogo”,到控制台打印出“outer”,这段时间发生了什么?

捕获与冒泡
第一阶段: 事件捕获
每个div都像一个纸盒子(俄罗斯套娃了解一下),外层div盒子里,有内层div盒子(月饼盒了解一下?)。那么如果你用手点这个纸盒子,肯定是外层的先接收到信号,外层纸盒子被点出一个凹槽(这个盒子比较软),这个凹槽的底部会碰到内层盒子,于是内层纸盒子接受到信号。
事件的捕获是由外而内的。
第二阶段: 事件冒泡
冒泡这个词本身就解释了这个过程的顺序,肯定是从里往外冒啊。

也就是,当鼠标点击到“gogo”后:

  1. 外层div先捕获到这个点击事件
  2. 然后内层div捕获到这个点击事件
  3. 内层div(的监听器)处理这次事件
  4. 外层div(的监听器)处理这个点击事件

新的问题

按上面的说法,事件处理的监听器应该是由内而外执行啊,但是上面的代码(当addEventListener第三个参数为true时)并不符合这个规则。有一个错误的结论是,当addEventListener第三个参数为true时,监听器会在捕获阶段就执行,false时,在冒泡阶段执行,其实根据这个结论是完全解释得通上面所有的代码的,特别是上面最后一段。这也是很多人正在犯的错误,下面这段代码证明了这个结论的错误

(代码五)

<div id="outer">
	<div id="inner">
		gogo
	</div>
</div>

get('inner').addEventListener('click',function(e){
	console.log('inner,false');
},false);
get('inner').addEventListener('click',function(e){
	console.log('inner,true');
},true);

get('outer').addEventListener('click',function(e){
	console.log('outer,false');
},false);
get('outer').addEventListener('click',function(e){
	console.log('outer,true');
},true);

错误结论的结果:

outer,true
inner,true
inner,false
outer,false

实际的结果

outer,true
inner,false
inner,true
outer,false

当我第一次看到这个结果我可是一脸蒙蔽。于是,我去mdn看了一下第三个参数,有下述文字:

(第三个参数是)A Boolean indicating whether events of this type will be dispatched to the registered listener before being dispatched to any EventTarget beneath it in the DOM tree.

我对着这句话看了好几分钟,又对照中文版也看了好几分钟,其意没现还不是因为没读百遍?于是我又读了好几分钟,有如下心得:

  • 首先第三个参数是个boolean
  • 然后一个boolean能代表什么,当然是“是否”喽
  • 于是看到了whether,那么whether what? 我开始以为是this type will be的be,但是实际上是后面的before
  • 于是得出了这个whether的正反面
  • 正面:events (of this type) will be dispatched to the registered listener before being dispatched to any EventTarget (beneath it in the DOM tree). (这种事件会被dispatch到注册了的监听器上(dispatch了就会马上执行),before 被dispatch到内层dom结点(就是那个 beneath it in the dom tree)的其他eventTarget上)
  • 反面:events (of this type) will be dispatched to the registered listener after(或者说not before) being dispatched to any EventTarget (beneath it in the DOM tree).

谁先谁后不如排个序,会看起来更明了一点(本文最重要的结论,如果上面的我没解释明白……记住下面这两行应该是有好处的):

  • 事件被dispatch到监听器上(马上会执行)
  • 然后,事件被dispatch到内层dom结点的eventTarget上(只是到了target上,并没交给listener,也就是不会马上被执行)

也就是,在往内层传递点击事件之前,监听器被执行,也就是先执行外层div的监听器,内层才会接收到点击事件。

回到起点

前两段代码

普通的事件捕获和普通的事件冒泡

第三段代码

当鼠标点到“gogo”时,outer先接收到了“点击”,因为它被注册了一个监听器(通过addEventListener),而且第三个参数是true,所以应该先执行自己的监听器,再往middle传事件。(也就是在middle没捕获“点击”之前,outer的监听器就已经被执行了)。于是……,没问题。

第四段代码

和第三段代码差不多,于是也没什么问题。

第五段代码

这是本文最后一个问题。因为addEventListener()的第三个参数是决定先往内层结点传还是先自己处理监听器,所以当没有下级结点,这个参数还有什么意义?,这时候,谁先注册事件,谁就先执行(这是本文第二重要的结论)[嘿嘿,想不到吧.jpg]。

发布了44 篇原创文章 · 获赞 25 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/csdn372301467/article/details/82116847