事件捕获、事件冒泡以及事件代理

9,237 阅读5分钟

DOM事件流

话不多说,先附上W3C链接:www.w3.org/TR/DOM-Leve…
image.png
上图是W3C标准的DOM事件流模型图,从图中可以看出,元素事件响应在DOM树中是从顶层的Window开始“流向”目标元素(),然后又从目标元素“流向”顶层的Window。

通常,我们将这种事件流向分为三个阶段:捕获阶段,目标阶段,冒泡阶段

  • 捕获阶段是指事件响应从最外层的Window开始,逐级向内层前进,直到具体事件目标元素。在捕获阶段,不会处理响应元素注册的冒泡事件。
  • 目标阶段指触发事件的最底层的元素,如上图中的。
  • 冒泡阶段与捕获阶段相反,事件的响应是从最底层开始一层一层往外传递到最外层的Window。


现在,我们就可以知道,DOM事件流的三个阶段是先捕获阶段,然后是目标阶段,最后才是冒泡阶段。我们时常面试所说的先捕获后冒泡也由此而来。事件代理就是利用事件冒泡或事件捕获的机制把一系列的内层元素事件绑定到外层元素。

事件冒泡和事件捕获

实际操作中,我们可以通过 element.addEventListener() 设置一个元素的事件模型为冒泡事件或者捕获事件。
先来看一下 addEventListener 函数的语法:

element.addEventListener(type, listener, useCapture)
  • type
    监听事件类型的字符串
  • listener
    事件监听回调函数,即事件触发后要处理的函数
  • useCapture
    默认值false,表示事件冒泡;设为true时,表示事件捕获

事件冒泡举例

<div id="a" style="width: 100%; height: 300px;background-color: antiquewhite;">
	a
	<div id="b" style="width: 100%; height: 200px;background-color: burlywood;">
		b
		<div id="c" style="width: 100%; height: 100px;background-color: cornflowerblue;">
			c
		</div>
	</div>
</div>
<script>
	var a = document.getElementById('a')
	var b = document.getElementById('b')
	var c = document.getElementById('c')
  //注册冒泡事件监听器
	a.addEventListener('click', () => {console.log("冒泡a")})
	b.addEventListener('click', () => {console.log('冒泡b')})
	c.addEventListener('click', () => {console.log("冒泡c")})
</script>

当我们点击c时,执行结果如下:
image.png
冒泡事件的执行顺序为:c -> b -> a

事件捕获举例

<div id="a" style="width: 100%; height: 300px;background-color: antiquewhite;">
	a
	<div id="b" style="width: 100%; height: 200px;background-color: burlywood;">
		b
		<div id="c" style="width: 100%; height: 100px;background-color: cornflowerblue;">
			c
		</div>
	</div>
</div>
<script>
	var a = document.getElementById('a')
	var b = document.getElementById('b')
	var c = document.getElementById('c')
  //注册捕获事件监听器
  a.addEventListener('click', () => {console.log("捕获a")}, true)
  b.addEventListener('click', () => {console.log('捕获b')}, true)
  c.addEventListener('click', () => {console.log("捕获c")}, true)
</script>

当我们点击c时,运行结果如下:
image.png
捕获事件的执行顺序为:a -> b -> c

事件捕获VS事件冒泡(修改)

这篇文章因为写的比较早,当时浏览器的测试版本已经忘了,不过这一章节内容在现在的浏览器有不同的表现,所以现在重新修改一下这一章节的内容,这一章节的测试浏览器版本为 Chrome 版本 111.0.5563.110(正式版本) (arm64)

我们将上述的代码a,b,c三个元素都注册捕获和冒泡事件,并以元素c作为触发事件的主体,即事件流中的目标阶段。

<div id="a" style="width: 100%; height: 300px;background-color: antiquewhite;">
	a
	<div id="b" style="width: 100%; height: 200px;background-color: burlywood;">
		b
		<div id="c" style="width: 100%; height: 100px;background-color: cornflowerblue;">
			c
		</div>
	</div>
</div>
<script>
	var a = document.getElementById('a')
	var b = document.getElementById('b')
	var c = document.getElementById('c')
	a.addEventListener('click', () => {console.log("冒泡a")})
	b.addEventListener('click', () => {console.log('冒泡b')})
	c.addEventListener('click', () => {console.log("冒泡c")})
	a.addEventListener('click', () => {console.log("捕获a")}, true)
	b.addEventListener('click', () => {console.log('捕获b')}, true)
	c.addEventListener('click', () => {console.log("捕获c")}, true)
</script>

a,b,c三个元素都是先注册冒泡事件再注册捕获事件,当我们点击c时,执行结果又是如何?请看下图。 image.png

从执行结果可以看到,a,b两个元素的事件响应都是先捕获后冒泡的,但对于触发事件的目标元素c,事件的响应也是遵循先捕获后冒泡的规则(最新答案)

如果我们对于某个元素需要先执行冒泡事件再执行捕获事件,我们可以在注册监听器时通过定时起暂缓执行捕获事件,等冒泡事件执行完之后,在执行捕获事件, 例子如下:

<div id="a" style="width: 100%; height: 300px;background-color: antiquewhite;">
	a
	<div id="b" style="width: 100%; height: 200px;background-color: burlywood;">
		b
		<div id="c" style="width: 100%; height: 100px;background-color: cornflowerblue;">
			c
		</div>
	</div>
</div>
<script>
	var b = document.getElementById('b')
	var c = document.getElementById('c')
	b.addEventListener('click', () => {console.log('冒泡b')})
	c.addEventListener('click', () => {console.log("冒泡c")})
	b.addEventListener('click', () => {
            // 非目标元素,注册定时器,暂缓执行
          setTimeout(() => {
            console.log('捕获b')
          })
        }, true)
        c.addEventListener('click', () => {
          // 目标元素,注册定时器,暂缓执行
          setTimeout(() => {
            console.log("捕获c")
          })
        }, true)
</script>

image.png

上面这个例子,就是通过定时器来暂缓执行捕获事件,从而实现事件执行先冒泡后捕获的效果

事件代理(事件委托)

我们知道了事件冒泡和事件捕获的原理,那么对于事件委托就比较容易理解了。
重复一遍,**事件代理就是利用事件冒泡或事件捕获的机制把一系列的内层元素事件绑定到外层元素。**至于为什么通常我们说事件代理是利用事件冒泡的机制来实现的,只是大家习以为常而已。

<ul id="item-list">
	<li>item1</li>
	<li>item2</li>
	<li>item3</li>
	<li>item4</li>
</ul>

对于上述的列表元素,我们希望将用户点击了哪个item打印出来,通常我们可以给每个item注册点击事件监听器,但是需要对每个元素进行事件监听器的注册;但是通过事件代理,我们可以将多个事件监听器减少为一个,这样就减少代码的重复编写了。
利用事件冒泡或事件捕获实现事件代理:

var items = document.getElementById('item-list');
//事件捕获实现事件代理
items.addEventListener('click', (e) => {console.log('捕获:click ',e.target.innerHTML)}, true);
//事件冒泡实现事件代理
items.addEventListener('click', (e) => {console.log('冒泡:click ',e.target.innerHTML)}, false);

当点击列表中的item时,执行结果如下:
image.png

因此,事件代理既可以通过事件冒泡来实现,也可以通过事件捕获来实现

总结

以上的东西总结起来就是有以下几点:

  • DOM事件流有3个阶段:捕获阶段,目标阶段,冒泡阶段;三个阶段的顺序为:捕获阶段——目标阶段——冒泡阶段;
  • (更新)✅不管对于非目标阶段或者目标阶段的元素,事件响应执行顺序都是遵循先捕获后冒泡的原则;通过使用定时器暂缓执行捕获事件,可以达到先冒泡后捕获的效果;
  • (错误)对于目标元素,事件响应执行顺序根据的事件的执行顺序执行;
  • 事件捕获是从顶层的Window逐层向内执行,事件冒泡则相反;
  • 事件委托(事件代理)是根据事件冒泡或事件捕获的机制来实现的。

题外话

一直不理解事件流的模型为什么是先捕获后冒泡?直到看到第一张图的那个Window,才明白为什么是先捕获后冒泡了。因为Window对象是直接面向用户的,那么用户触发一个事件,如点击事件,肯定是用window对象开始的,所以自然就是先捕获后冒泡啦。