JavaScript Event preventDefault和stopPropagation

前言

对于JS的事件传递还是比较陌生,所以打算好好理一理preventDefault和stopPropagation的用法,彻底告别模糊,提升自己的前端段位!

栗子

在以下示例中,单击Web浏览器中的超链接将触发事件的流程(执行事件监听器)和事件目标的默认操作(打开新选项卡)。

HTML:

1
2
3
<div id="a">
<a id="b" href="http://www.google.com/" target="_blank">Google</a>
</div>

JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var el = document.getElementById("c");
function capturingOnClick1(ev) {
el.innerHTML += "DIV event capture<br>";
}
function capturingOnClick2(ev) {
el.innerHTML += "A event capture<br>";
}
function bubblingOnClick1(ev) {
el.innerHTML += "DIV event bubbling<br>";
}
function bubblingOnClick2(ev) {
el.innerHTML += "A event bubbling<br>";
}
// The 3rd parameter useCapture makes the event listener capturing (false by default)
document.getElementById("a").addEventListener("click", capturingOnClick1, true);
document.getElementById("b").addEventListener("click", capturingOnClick2, true);
document.getElementById("a").addEventListener("click", bubblingOnClick1, false);
document.getElementById("b").addEventListener("click", bubblingOnClick2, false);

输出

event captureA event captureA event bubblingDIV event bubbling
1
2
3
4
DIV event capture
A event capture
A event bubbling
DIV event bubbling
capturingOnClick1函数添加stopPropagation()
1
2
3
4
function capturingOnClick1(ev) {
el.innerHTML += "DIV event capture<br>";
ev.stopPropagation();
}

结果只输出

1
DIV event capture

事件侦听器阻止了事件的进一步向下和向上传播。但是,这并没有阻止默认操作(打开新标签页)。

capturingOnClick2函数添加stopPropagation()
1
2
3
4
function capturingOnClick2(ev) {
el.innerHTML += "A event capture<br>";
ev.stopPropagation();
}

或者

1
2
3
4
function bubblingOnClick2(ev) {
el.innerHTML += "A event bubbling<br>";
ev.stopPropagation();
}

结果

1
2
3
DIV event capture
A event capture
A event bubbling

这是因为两个事件侦听器都注册在同一事件目标上。事件侦听器阻止了事件的进一步向上传播。但是,它们并没有阻止默认操作(打开新标签页)。

preventDefault()添加到任何函数中
1
2
3
4
function capturingOnClick1(ev) {
el.innerHTML += "DIV event capture<br>";
ev.preventDefault();
}

结果照常输出

1
2
3
4
DIV event capture
A event capture
A event bubbling
DIV event bubbling

但它阻止打开新标签页

原理解析

事件顺序

基本问题很简单,假设元素内部有一个元素,如下:

1
2
3
4
5
6
7
-----------------------------------
| element1 |
| ------------------------- |
| |element2 | |
| ------------------------- |
| |
-----------------------------------

两者都有一个onClick事件处理程序。如果用户单击element2,他将在element1和element2中都引起click事件。但是哪个事件首先触发?应该先执行哪个事件处理程序?换句话说,事件顺序是什么?

两种事件模型

在过去,Netscape和Microsoft得出了不同的结论。

  • Netscape说,element1上的事件首先发生。这称为事件捕获***(capturing)***。

  • Microsoft坚持认为element2上的事件优先。这称为事件冒泡**(bubbling)**。

这两个事件顺序完全相反。 Explorer仅支持事件冒泡。 Mozilla,Opera 7和Konqueror都支持。旧版Opera和iCab都不支持。

事件捕获(capturing)

使用事件捕获时

1
2
3
4
5
6
7
8
               | |
---------------| |-----------------
| element1 | | |
| -----------| |----------- |
| |element2 \ / | |
| ------------------------- |
| Event CAPTURING |
-----------------------------------

element1的事件处理程序首先触发,element2的事件处理程序最后触发。

事件冒泡(bubbling)

使用事件冒泡时

1
2
3
4
5
6
7
8
               / \
---------------| |-----------------
| element1 | | |
| -----------| |----------- |
| |element2 | | | |
| ------------------------- |
| Event BUBBLING |
-----------------------------------

element2的事件处理程序首先触发,element1的事件处理程序最后触发。

W3C 事件模型

W3C非常明智地决定在这场斗争中处于中间位置。 W3C事件模型中发生的任何事件都首先被捕获,直到到达目标元素,然后再次冒泡。

1
2
3
4
5
6
7
8
                 | |  / \
-----------------| |--| |-----------------
| element1 | | | | |
| -------------| |--| |----------- |
| |element2 \ / | | | |
| -------------------------------- |
| W3C event model |
------------------------------------------`

Web开发人员可以选择是在捕获阶段还是冒泡阶段中注册事件处理程序。这是通过“高级模型”页面上说明的addEventListener()方法完成的。如果最后一个参数为true,则为捕获阶段设置事件处理程序,如果为false,则为冒泡阶段设置事件处理程序。

假设

1
2
element1.addEventListener('click',doSomething2,true)
element2.addEventListener('click',doSomething,false)

如果用户单击element2,则会发生以下情况:

  • 单击事件在捕获阶段开始。该事件查找element2的任何祖先元素是否具有用于捕获阶段的onclick事件处理程序。
  • 该事件在element1上找到一个。doSomething2()被执行。
  • 事件向下传播到目标本身,找不到用于捕获阶段的事件处理程序。该事件进入其冒泡阶段并执行doSomething(),该事件已在冒泡阶段注册到element2。
  • 事件再次向上传播,并检查目标的任何祖先元素是否具有用于冒泡阶段的事件处理程序。事实并非如此,因此什么也没有发生。

相反

1
2
element1.addEventListener('click',doSomething2,false)
element2.addEventListener('click',doSomething,false)

现在,如果用户单击element2,则会发生以下情况:

  • 单击事件在捕获阶段开始。该事件将查找element2的任何祖先元素是否具有用于捕获阶段的onclick事件处理程序,而找不到任何事件处理程序。
  • 事件向下传播到目标本身。该事件进入其冒泡阶段并执行doSomething(),该事件已在冒泡阶段注册到element2。
  • 事件再次向上传播,并检查目标的任何祖先元素是否具有用于冒泡阶段的事件处理程序。
  • 该事件在element1上找到一个。现在执行doSomething2()

与传统模型的兼容性

在支持W3C DOM的浏览器中,传统的事件注册

1
element1.onclick = doSomething2;

被视为冒泡阶段的注册。

使用事件冒泡

很少有Web开发人员自觉使用事件捕获或冒泡。在当今的网页中,根本没有必要让冒泡事件由多个不同的事件处理程序处理。用户可能会因单击鼠标后发生的几件事而感到困惑,并且通常您希望将事件处理脚本分开。当用户单击某个元素时,会发生某些事情,而当用户单击另一个元素时,会发生其他事情。

当然,这种情况将来可能会改变,因此最好可以使用向前兼容的模型。但是,今天事件捕获和冒泡的主要实际用途是默认功能的注册。

总是会发生

您首先需要了解的是,事件捕获或冒泡总是会发生。如果您为整个document定义常规的onclick事件处理程序

1
2
document.onclick = doSomething;
if (document.captureEvents) document.captureEvents(Event.CLICK);

document中任何元素上的任何click事件最终都会冒泡到document中,从而触发此常规事件处理程序。仅当以前的事件处理脚本明确命令事件停止冒泡时,它才不会冒泡到 document。

使用

因为任何事件最终都出现在 document 上,所以默认事件处理程序成为可能。假设您有此页面:

1
2
3
4
5
6
7
8
9
10
11
------------------------------------
| document |
| --------------- ------------ |
| | element1 | | element2 | |
| --------------- ------------ |
| |
------------------------------------

element1.onclick = doSomething;
element2.onclick = doSomething;
document.onclick = defaultFunction;

现在,如果用户单击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
2
3
4
5
6
function doSomething(e)
{
if (!e) var e = window.event;
e.cancelBubble = true;
if (e.stopPropagation) e.stopPropagation();
}

在不支持该功能的浏览器中设置cancelBubble属性不会有任何问题。浏览器耸耸肩并创建属性。当然,它实际上并不能消除冒泡,但是作业本身是安全的。

当前目标

如我们前面所见,事件具有一个target或srcElement,其中包含对该事件发生所在元素的引用。在我们的示例中,这是element2,因为用户单击了它。

非常重要的一点是要理解,在捕获和冒泡阶段(如果有),该目标不会改变:它始终是对element2的引用。

但是,假设我们注册了以下事件处理程序:

1
2
element1.onclick = doSomething;
element2.onclick = doSomething;

如果用户单击element2,则doSomething()将执行两次。但是,您如何知道当前正在处理该事件的HTML元素? target / srcElement不提供任何线索,它们始终引用element2,因为它是事件的原始来源。

为了解决此问题,W3C添加了currentTarget属性。它包含对事件当前正在处理的HTML元素的引用:正是我们所需要的。不幸的是,Microsoft模型不包含类似的属性。

您也可以使用this关键字。在上面的示例中,它引用处理事件的HTML元素,就像currentTarget一样。

Microsoft模式的问题

但是,当您使用Microsoft事件注册模型时,此关键字并不引用HTML元素。加上Microsoft模型中缺少类似于currentTarget的属性,这意味着如果您这样做

1
2
element1.attachEvent('onclick',doSomething)
element2.attachEvent('onclick',doSomething)

您不知道当前哪个HTML元素处理该事件。这是Microsoft事件注册模型中最严重的问题,对我来说,这是一个从不使用它的理由,即使在仅IE / Win的应用程序中也是如此。

我希望微软能尽快添加类似于currentTarget的属性,甚至可以遵循该标准? Web开发人员需要此信息。

参考

Event Order