事件冒泡是您在JavaScript旅途中可能遇到的一个术语。 它涉及到一个元素嵌套在第二个元素内时事件处理程序被调用的顺序,并且两个元素都注册了同一事件的侦听器(例如,单击)。
但是事件冒泡只是谜团之一。 通常将其与事件捕获和事件传播结合使用。 对这三个概念的深刻理解对于使用JavaScript处理事件至关重要-例如,如果您希望实现事件委托模式 。
在这篇文章中,我将解释这些术语中的每一个,并演示它们如何组合在一起。 我还将向您展示对JavaScript事件流的基本理解如何使您可以对应用程序进行细粒度的控制。 请注意,这不是事件的入门,因此假设您已熟悉该主题。 如果您想了解一般事件的更多信息,为什么不阅读我们的书: JavaScript:Ninja的新手 。
什么是事件传播?
让我们从事件传播开始。 这是事件冒泡和事件捕获的总称。 考虑典型的标记以建立链接图像列表,例如,缩略图库:
<ul>
<li><a href="..."><img src="..." alt=""></a>
<li><a href="..."><img src="..." alt=""></a>
...
<li><a href="..."><img src="..." alt=""></a>
</ul>
单击图像不仅会为相应的IMG
元素生成click
事件,还会为父级A
,祖父LI
等等生成click
事件,一直到元素的所有祖先,直到终止于window
对象为止。
在DOM术语中,图片是事件目标 ,即点击所产生的最里面的元素。 事件目标及其祖先,从其父节点一直到window
对象,在DOM树中形成一个分支。 例如,在图片库中,此分支将由以下节点组成: IMG
, A
, LI
, UL
, BODY
, HTML
, document
, window
。
请注意,
window
实际上不是DOM节点,而是实现了EventTarget
接口,因此,为简单起见,我们将其作为文档对象的父节点来处理。
该分支很重要,因为它是事件传播(或流动)的路径。 这种传播是为给定事件类型调用所有侦听器的过程,该事件类型附加到分支上的节点。 每个侦听器都将被一个event
对象调用,该对象收集与event
相关的信息(稍后会对此进行更多介绍)。
请记住,可以在同一事件类型的节点上注册多个侦听器。 当传播到达一个这样的节点时,将按照其注册顺序调用侦听器。
还应注意,分支确定是静态的,也就是说,它是在事件的初始派发时建立的。 事件处理期间发生的树修改将被忽略。
从窗口到事件目标再向后传播是双向的。 这种传播可以分为三个阶段:
- 从窗口到事件目标父对象:这是捕获阶段
- 事件目标本身:这是目标阶段
- 从事件目标父级回到窗口: 气泡阶段
这些阶段的区别在于所调用的侦听器的类型。
事件捕获阶段
在此阶段,仅调用捕获器侦听器,即,使用addEventListener的第三个参数的true
值注册的侦听器:
el.addEventListener('click', listener, true)
如果省略此参数,则其默认值为false,并且侦听器不是捕获器。
因此,在此阶段中,仅调用从窗口到事件目标父对象的路径中找到的捕获器。
活动目标阶段
在此阶段,将调用在事件目标上注册的所有侦听器,无论其捕获标志的值如何。
事件冒泡阶段
在事件冒泡阶段,将仅呼叫非捕获者。 也就是说,只有针对addEventListener()
的第三个参数以false
值注册的侦听器:
el.addEventListener('click', listener, false) // listener doesn't capture
el.addEventListener('click', listener) // listener doesn't capture
请注意,尽管所有事件都通过捕获阶段, focus
, blur
, load
和其他一些事件流到事件目标,但不要冒泡。 也就是说,他们的旅行在目标阶段之后停止。
因此,在传播结束时,分支上的每个侦听器仅被调用一次。
事件冒泡并不会针对每种事件发生。 在传播期间,侦听器可以通过读取event
对象的.bubbles
布尔属性来了解事件是否冒泡。
下图显示了W3C UIEvents规范中的三个事件流阶段。
版权所有©2016万维网联盟(MIT,ERCIM,京王,北航) 。
访问传播信息
我已经提到了event
对象的.bubbles
属性。 此对象提供了许多其他属性,供侦听器用来访问有关传播的信息。
- e.target引用事件目标。
- e.currentTarget是在其上注册正在运行的侦听器的节点。 这是与侦听器调用上下文相同的值,即
this
关键字引用的值。 - 我们甚至可以使用e.eventPhase找出当前阶段。 它是指一个三个整数
Event
构造常量CAPTURING_PHASE
,BUBBLING_PHASE
和AT_TARGET
。
付诸实践
让我们将上述概念付诸实践。 在下面的笔中,有五个嵌套的方形框,名为b0
… b4
。 最初,只有外部框b0
是可见的; 当鼠标指针悬停在上方时,将显示内部。 当我们单击一个框时,传播流的日志显示在右表中。
请参阅CodePen上的SitePoint( @SitePoint )的Pen jmXdpz 。
甚至可以在框外单击:在这种情况下,事件目标将是BODY
或HTML
元素,具体取决于单击屏幕的位置。
停止传播
通过调用事件对象的stopPropagation方法,可以在任何侦听器中停止事件传播。 这意味着将不会调用在传播路径上跟随当前目标的节点上注册的所有侦听器。 相反,当前目标上附加的所有其他其余侦听器仍将接收该事件。
我们可以使用上一个演示的简单fork来检查此行为,只需在其中一个stopPropagation()
插入对stopPropagation()
的调用即可。 在这里,我们将这个新的侦听器作为捕获器添加到window
注册的回调列表的前面:
window.addEventListener('click', e => { e.stopPropagation(); }, true);
window.addEventListener('click', listener('c1'), true);
window.addEventListener('click', listener('c2'), true);
window.addEventListener('click', listener('b1'));
window.addEventListener('click', listener('b2'));
这样,无论单击什么框,传播都会提前停止,仅到达window
上的捕获器侦听window
。
停止立即传播
如其名称所示, stopImmediatePropagation会立即刹车,甚至阻止当前侦听器的同级接收该事件。 我们可以看到这与最后一支笔的最小变化 :
window.addEventListener('click', e => { e.stopImmediatePropagation(); }, true);
window.addEventListener('click', listener('c1'), true);
window.addEventListener('click', listener('c2'), true);
window.addEventListener('click', listener('b1'));
window.addEventListener('click', listener('b2'));
现在,日志表中没有任何输出, c1
和c2
窗口捕获器行也没有输出,因为传播在执行新侦听器后停止。
活动取消
某些事件与浏览器在传播结束时执行的默认操作相关联。 例如,单击链接元素或单击表单提交按钮会使浏览器导航到新页面或分别提交表单。
通过在侦听器中调用事件对象的另一种方法e.preventDefault , 可以避免通过事件取消执行此类默认操作。
结论
借此,我希望向您展示了事件冒泡和事件捕获在JavaScript中是如何工作的。 如果您有任何疑问或意见,我们很高兴在下面的讨论中听到他们的意见。
参考文献
Yaphi Berhanu和Dominic Myers对这篇文章进行了同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!