前言
对于JS的事件传递还是比较陌生,所以打算好好理一理preventDefault和stopPropagation的用法,彻底告别模糊,提升自己的前端段位!
栗子
在以下示例中,单击Web浏览器中的超链接将触发事件的流程(执行事件监听器)和事件目标的默认操作(打开新选项卡)。
HTML:
1 | <div id="a"> |
JavaScript:
1 | var el = document.getElementById("c"); |
输出
1 | DIV event capture |
向capturingOnClick1函数添加stopPropagation()
1 | function capturingOnClick1(ev) { |
结果只输出
1 | DIV event capture |
事件侦听器阻止了事件的进一步向下和向上传播。但是,这并没有阻止默认操作(打开新标签页)。
向capturingOnClick2函数添加stopPropagation()
1 | function capturingOnClick2(ev) { |
或者
1 | function bubblingOnClick2(ev) { |
结果
1 | DIV event capture |
这是因为两个事件侦听器都注册在同一事件目标上。事件侦听器阻止了事件的进一步向上传播。但是,它们并没有阻止默认操作(打开新标签页)。
将preventDefault()添加到任何函数中
1 | function capturingOnClick1(ev) { |
结果照常输出
1 | DIV event capture |
但它阻止打开新标签页
原理解析
事件顺序
基本问题很简单,假设元素内部有一个元素,如下:
1 | ----------------------------------- |
两者都有一个onClick事件处理程序。如果用户单击element2,他将在element1和element2中都引起click事件。但是哪个事件首先触发?应该先执行哪个事件处理程序?换句话说,事件顺序是什么?
两种事件模型
在过去,Netscape和Microsoft得出了不同的结论。
-
Netscape说,element1上的事件首先发生。这称为事件捕获***(capturing)***。
-
Microsoft坚持认为element2上的事件优先。这称为事件冒泡**(bubbling)**。
这两个事件顺序完全相反。 Explorer仅支持事件冒泡。 Mozilla,Opera 7和Konqueror都支持。旧版Opera和iCab都不支持。
事件捕获(capturing)
使用事件捕获时
1 | | | |
element1的事件处理程序首先触发,element2的事件处理程序最后触发。
事件冒泡(bubbling)
使用事件冒泡时
1 | / \ |
element2的事件处理程序首先触发,element1的事件处理程序最后触发。
W3C 事件模型
W3C非常明智地决定在这场斗争中处于中间位置。 W3C事件模型中发生的任何事件都首先被捕获,直到到达目标元素,然后再次冒泡。
1 | | | / \ |
Web开发人员可以选择是在捕获阶段还是冒泡阶段中注册事件处理程序。这是通过“高级模型”页面上说明的addEventListener()方法完成的。如果最后一个参数为true,则为捕获阶段设置事件处理程序,如果为false,则为冒泡阶段设置事件处理程序。
假设
1 | element1.addEventListener('click',doSomething2,true) |
如果用户单击element2,则会发生以下情况:
- 单击事件在捕获阶段开始。该事件查找element2的任何祖先元素是否具有用于捕获阶段的onclick事件处理程序。
- 该事件在element1上找到一个。
doSomething2()被执行。 - 事件向下传播到目标本身,找不到用于捕获阶段的事件处理程序。该事件进入其冒泡阶段并执行
doSomething(),该事件已在冒泡阶段注册到element2。 - 事件再次向上传播,并检查目标的任何祖先元素是否具有用于冒泡阶段的事件处理程序。事实并非如此,因此什么也没有发生。
相反
1 | element1.addEventListener('click',doSomething2,false) |
现在,如果用户单击element2,则会发生以下情况:
- 单击事件在捕获阶段开始。该事件将查找element2的任何祖先元素是否具有用于捕获阶段的onclick事件处理程序,而找不到任何事件处理程序。
- 事件向下传播到目标本身。该事件进入其冒泡阶段并执行
doSomething(),该事件已在冒泡阶段注册到element2。 - 事件再次向上传播,并检查目标的任何祖先元素是否具有用于冒泡阶段的事件处理程序。
- 该事件在element1上找到一个。现在执行
doSomething2()。
与传统模型的兼容性
在支持W3C DOM的浏览器中,传统的事件注册
1 | element1.onclick = doSomething2; |
被视为冒泡阶段的注册。
使用事件冒泡
很少有Web开发人员自觉使用事件捕获或冒泡。在当今的网页中,根本没有必要让冒泡事件由多个不同的事件处理程序处理。用户可能会因单击鼠标后发生的几件事而感到困惑,并且通常您希望将事件处理脚本分开。当用户单击某个元素时,会发生某些事情,而当用户单击另一个元素时,会发生其他事情。
当然,这种情况将来可能会改变,因此最好可以使用向前兼容的模型。但是,今天事件捕获和冒泡的主要实际用途是默认功能的注册。
总是会发生
您首先需要了解的是,事件捕获或冒泡总是会发生。如果您为整个document定义常规的onclick事件处理程序
1 | document.onclick = doSomething; |
document中任何元素上的任何click事件最终都会冒泡到document中,从而触发此常规事件处理程序。仅当以前的事件处理脚本明确命令事件停止冒泡时,它才不会冒泡到 document。
使用
因为任何事件最终都出现在 document 上,所以默认事件处理程序成为可能。假设您有此页面:
1 | ------------------------------------ |
现在,如果用户单击element1或2,则将执行doSomething()。如果需要,可以在此处停止事件传播。如果您不这样做,则事件会上升到defaultFunction()。如果用户单击其他任何地方,还将执行defaultFunction()。有时这可能很有用。
在拖放脚本中,必须设置document范围的事件处理程序。通常,上层的mousedown事件会选择该层并使之响应mousemove事件。尽管通常在层上注册mousedown以避免浏览器错误,但是其他两个事件处理程序都必须在document范围内。
记住浏览器学的第一定律:任何事情都有可能发生,并且通常在您最没有准备的情况下才会发生。因此,用户可能会非常疯狂地移动鼠标,而脚本无法跟上,以至于鼠标不再位于图层上。
- 如果
onmousemove事件处理程序已注册到图层,则该图层不再对鼠标移动做出反应,从而引起混乱。 - 如果
onmouseup事件处理程序已在图层上注册,则不会捕获此事件,因此即使用户认为他放下了该图层,该图层也会继续对鼠标移动做出反应。这引起了更多的混乱。
因此,在这种情况下,事件冒泡非常有用,因为在文档级别注册事件处理程序可确保始终执行它们。
禁用
但是通常您想关闭所有捕获和冒泡功能,以防止功能相互干扰。此外,如果您的文档结构非常复杂(很多嵌套表等),则可以通过关闭冒泡来节省系统资源。浏览器必须遍历事件目标的每个祖先元素,以查看其是否具有事件处理程序。即使未找到,搜索仍然需要时间。
在Microsoft模型中,您必须将事件的cancelBubble属性设置为true。
1 | window.event.cancelBubble = true |
在W3C模型中,您必须调用事件的*stopPropagation()*方法。
1 | e.stopPropagation() |
这将停止事件在冒泡阶段的所有传播。要获得完整的跨浏览器体验,请执行
1 | function doSomething(e) |
在不支持该功能的浏览器中设置cancelBubble属性不会有任何问题。浏览器耸耸肩并创建属性。当然,它实际上并不能消除冒泡,但是作业本身是安全的。
当前目标
如我们前面所见,事件具有一个target或srcElement,其中包含对该事件发生所在元素的引用。在我们的示例中,这是element2,因为用户单击了它。
非常重要的一点是要理解,在捕获和冒泡阶段(如果有),该目标不会改变:它始终是对element2的引用。
但是,假设我们注册了以下事件处理程序:
1 | element1.onclick = doSomething; |
如果用户单击element2,则doSomething()将执行两次。但是,您如何知道当前正在处理该事件的HTML元素? target / srcElement不提供任何线索,它们始终引用element2,因为它是事件的原始来源。
为了解决此问题,W3C添加了currentTarget属性。它包含对事件当前正在处理的HTML元素的引用:正是我们所需要的。不幸的是,Microsoft模型不包含类似的属性。
您也可以使用this关键字。在上面的示例中,它引用处理事件的HTML元素,就像currentTarget一样。
Microsoft模式的问题
但是,当您使用Microsoft事件注册模型时,此关键字并不引用HTML元素。加上Microsoft模型中缺少类似于currentTarget的属性,这意味着如果您这样做
1 | element1.attachEvent('onclick',doSomething) |
您不知道当前哪个HTML元素处理该事件。这是Microsoft事件注册模型中最严重的问题,对我来说,这是一个从不使用它的理由,即使在仅IE / Win的应用程序中也是如此。
我希望微软能尽快添加类似于currentTarget的属性,甚至可以遵循该标准? Web开发人员需要此信息。