0%

面试官:stopImmediatePropagation与stopPropagation有什么区别?

开篇

image.png

stopImmediatePropagationstopPropagation是一对亲兄弟,他们两实现的功能是相似的,那么这两个函数的区别在哪里呢?如果你对于事件传播已经很了解,那么可以直接跳到总结部分,反之可以按照顺序阅读。

什么是DOM事件流?

早上起床你都会做些什么呢?闹钟响了,你摸索着掐掉闹钟,再眯一会,然后起床,刷牙,吃早餐……..,这一系列事件连接起来就构成了你起床的事件流。而DOM事件也是有一个流程的,DOM 事件标准描述了事件传播的 3 个阶段:

  1. 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
  2. 目标阶段(Target phase)—— 事件到达目标元素。
  3. 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。

    事件捕获

    DOM事件触发时(被触发DOM事件的这个元素被叫作事件源),浏览器会从根节点开始 由外到内 进行事件传播。即事件从文档的根节点流向目标对象节点,途中经过各个层次的DOM节点,最终到目标节点,完成事件捕获,

    事件冒泡

    事件冒泡与事件捕获顺序相反。事件捕获的顺序是从外到内,事件冒泡是从内到外。
    当事件传播到了目标阶段后,处于目标阶段的元素就会将接收到的时间向上传播,就是顺着事件捕获的路径,反着传播一次,逐级的向上传播到该元素的祖先元素,直到window对象。

    引发事件的那个嵌套层级最深的元素被称为目标元素,可以通过 event.target 访问。注意与 this(=event.currentTarget)之间的区别:

    • event.target —— 是引发事件的“目标”元素,它在冒泡过程中不会发生变化。
    • this —— 是“当前”元素,其中有一个当前正在运行的处理程序。

    使用addEventListener监听事件

    使用EventTarget.addEventListeners可以指定的侦听器注册到EventTarget上,我们可以监听页面上发生的诸多事件(mousedown,click, scroll等),当事件发生时就可以调用相应的函数进行事件处理。

它接收三个参数:分别是type, listener, options, 其中type表示监听事件类型的字符串,listener通常传入的是一个函数,在相应事件发生时调用它进行事件处理,options是一个与listener有关的可选参数对象,可用的参数有capture,once,passive, signal,
在本文中比较关注的是capture, 默认为false, 代表该侦听器在时间冒泡阶段触发,反之在事件捕获阶段触发。

如何阻止事件传播?

很经典的一个问题是如何阻止事件冒泡,很多人应该马上就能想到使用event.cancleBuble = true或者使用event.stopPropagation(), 这的确能够阻止事件冒泡。其实更准确的说,应该是阻止了事件传播, 因为这两者不仅仅可以在冒泡阶段使用,也可以在捕获阶段使用。看下面这一个例子。

Event.cancelBubble 属性是 Event.stopPropagation()的一个曾用名。在从事件处理程序返回之前将其值设置为true可阻止事件的传播。

dom结构如下:

1
2
3
4
5
6
7
8
9
<div class="A" style="font-size:30px">
A
<div class="B" style="font-size:30px">
B
<p style="font-size:30px">
&#128513;P
</p>
</div>
</div>

image.png

script部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const p = document.querySelector('p')
p.addEventListener("mousedown", (event) => {
// event.cancelBubble = true
event.stopPropagation()
console.log("我是p元素上被绑定的第一个监听函数: mousedown");
}, false);

p.addEventListener("mousedown", (event) => {
console.log("我是p元素上被绑定的第二个监听函数: mousedown");
}, false);
document.querySelector(".A").addEventListener("mousedown", (event) => {
event.cancelBubble = true
console.log("我是div & class A元素");
}, false);
document.querySelector(".B").addEventListener("mousedown", (event) => {
console.log("我是div & class B元素");
}, false);

上述事件传播的路径是:A -> B -> P -> B -> A, 选定<p></p>作为目标元素,每次试验时点击图中的笑脸所在的绿色区域。
image.png
我们在P元素上绑定了两个事件侦听器,并使用了event.cancleBuble = true(event.stopPropagation())成功阻止了事件冒泡。如果我在事件捕获阶段使用会怎么样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const p = document.querySelector('p')
p.addEventListener("mousedown", (event) => {
// event.cancelBubble = true
event.stopPropagation()
console.log("我是p元素上被绑定的第一个监听函数: mousedown");
}, false);

p.addEventListener("mousedown", (event) => {
console.log("我是p元素上被绑定的第二个监听函数: mousedown");
}, false);
document.querySelector(".A").addEventListener("mousedown", (event) => {
// event.cancelBubble = true
event.stopPropagation()
console.log("我是div & class A元素");
}, true);
document.querySelector(".B").addEventListener("mousedown", (event) => {
console.log("我是div & class B元素");
}, true);

image.png
从结果可以清楚地看到成功地阻止了事件传播。

那么现在让我们回到最初的问题:stopImmediatePropagation与stopPropagation有什么区别?
可以这样简单第理解:stopImmediatePropagationstopPropagation的加强版,前者不仅仅可以阻止事件传播,它还可以在多个事件监听器被附加到相同元素相同事件类型上时保证只有一个事件侦听器被调用。换句话说,当同一个元素上有多个事件侦听器侦听同一事件时,这些事件侦听器都会被按照初始添加顺序被调用,如上文中p元素就绑定了两个监听mousedown事件的侦听器;但如果在其中一个事件侦听器中使用了event.stopImmediatePropagation()的话,在该侦听器之后的侦听器就不会被调用了。
将代码改造成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const p = document.querySelector('p')
p.addEventListener("mousedown", (event) => {
console.log("我是p元素上被绑定的第一个监听函数: mousedown");
}, false);

p.addEventListener("mousedown", (event) => {
event.stopImmediatePropagation()
console.log("我是p元素上被绑定的第二个监听函数: mousedown");
}, false);

p.addEventListener("mousedown",(event) => {
console.log("我是p元素上被绑定的第三个监听函数: mousedown");
// 该监听函数排在上个函数后面,该函数不会被执行
}, false);

p.addEventListener('click', (event) => {
// 嘿嘿嘿,虽然监听的是同一元素,但却是不同事件!
console.log('我是p元素上被绑定的第四个监听函数: click')
}, false)

document.querySelector(".A").addEventListener("mousedown", (event) => {
console.log("我是div & class A元素");
}, false);

document.querySelector(".B").addEventListener("mousedown", (event) => {
console.log("我是div & class B元素");
}, false);

image.png
可以看到stopImmediatePropagation成功阻止了事件冒泡,A元素,B元素的侦听器都没有被调用,此外元素p上绑定了四个侦听器,其中三个侦听mousedown事件,剩下一个侦听click事件,我们在第二个侦听器中调用stopImmediatePropagation,前两个侦听器顺利调用,此外,由于最后一个侦听器侦听click事件,不受影响。

总结

  • 事件传播: 捕获 -> 目标元素 -> 冒泡
  • event
    • event.target —— 引发事件的层级最深的元素。
    • event.currentTarget(=this)—— 处理事件的当前元素(具有处理程序的元素)
    • event.eventPhase —— 当前阶段(capturing=1,target=2,bubbling=3)。
  • 阻止事件传播的方法:
    • event.stopPropagation()
    • event.cancelBubble = true
    • event.stopImmediatePropagation()
  • event.stopImmediatePropagation()
    • 可以阻止事件传播
    • 多个事件侦听器侦听同一元素同一事件时,执行event.stopImmediatePropagation()的侦听器之后的侦听器被忽略
    • 侦听同一元素的不同事件的侦听器不受影响
      最好自己动手试一试,加深理解。
      源代码在此处

    参考

    Event - MDN

冒泡和捕获

JavaScript事件捕获冒泡与捕获