DOM事件系列[2]

[2]事件传播与冒泡

1.事件传播

在web发展的早期,出现了如下的困惑:当我们在网页中点击某一区域的时候,是哪个DOM元素和我们进行相互交互。我们都知道,HTML采用的是树级嵌套结构,当我们点击某一个元素的时候,该元素的父元素同样被点击。可以说任何一个元素被点击,和标签都一样被点击。为了解决这个问题,网景Netscape和微软分别提出了不同的方法。

1.Netscape的解决方法:

  • 当元素事件被激活的时候,事件总是从DOM树的最高层对象开始依次往下。也就是说任何一个元素事件发生,最先开始捕获的事件的是document,然后是html,再接着则是body,依次往下。
  • 网景的这种处理方式称之为事件捕获即Event Capturing.

2.微软Internet Explorer的解决方法
IE的解决方法和网景的截然相反。当一个事件发生的时候,最先发生在绑定该事件的元素上,接着再依次向上冒泡。也就是当一个元素事件发生的时候,最先开始监测到事件的是绑定时间段额元素,接着才是该元素的父元素,依次到body、html,最后到document。
IE的这种事件处理方式称之为事件冒泡即Event Bubble.

后来W3C在定义DOM Level 2 Events的时候,将一个事件发生的过程定义了三个过程,分别是Event Capturing Phase,Event Target Phase,Event Bubble Phase。如下图所示:
事件传播示意图

在DOM Level 0 Events中的两种定义事件方式,在处理事件的时候都是采用事件冒泡的方式进行。而DOM Level 2 Events则可以选择事件是在捕获阶段还是冒泡阶段处理(由addEventListener()函数中的第三个参数决定)。

Example:

1.事件捕获、目标、冒泡三个阶段

1
2
3
4
5
6
7
<body>
<div>
<ul>
<li>Click Me!</li>
</ul>
</div>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var _div = document.querySelector("div");
var _ul = document.querySelector("ul");
var _li = document.querySelector("li");
//Capturing Phase
_div.addEventListener("click", callback, true);
_ul.addEventListener("click", callback, true);
_li.addEventListener("click", callback, true);
//Bubble Phase
_div.addEventListener("click", callback, false);
_ul.addEventListener("click", callback, false);
_li.addEventListener("click", callback, false);
//callback function
function callback(event){
var event = event || window.event;
var _currentTarget = event.currentTarget
alert(_currentTarget + " capturing");
}

具体案例点击查看

2.事件冒泡的使用

事件冒泡有其好处和弊端。在这一小节我们先来看看事件冒泡该如何使用?

当需要对多个元素进行事件绑定的时候,如果仍然采用之前我们所说的方法,则会造成内存负担,同时也会是JavaScript的执行速度越来越慢。如:

1
2
3
4
5
6
7
8
9
document.getElementById("help-btn").onclick = function(event){
openHelp();
};
document.getElementById("save-btn").onclick = function(event){
saveDocument();
};
document.getElementById("undo-btn").onclick = function(event){
undoChanges();
};

如果有100个元素需要绑定事件,按照这种方法我们需要写100个事件绑定,这显然是不现实的。对于这种情况我们应该使用事件代理(Event Delegation,又译为:事件委托)的方式来解决。Event Delegation利用的原理就是事件冒泡Bubbling。

事件代理就是利用事件冒泡在DOM最高层次(通常是document)对事件进行处理。当我们需要对很多元素添加事件的时候,可以通过将事件添加到它们的父节点而将事件委托给父节点来触发处理函数。
当事件在父节点处理的时候,我们需要知道的是哪个子节点触发了事件,这可以通过子节点的特性进行判断,如target.id子节点的id,target.className子节点的类名,target.nodeName子节点的名称等。
所以对于上面的例子,我们可以使用事件代理改写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
document.onclick = function(event){
//IE不会传入event参数
var event = event || window.event;
//IE 使用的是srcElement作为原始事件对象
var target = event.target || event.srcElement;
switch(target.id){
case "help-btn":
openHelp();
break;
case "save-btn":
saveDocument();
break;
case "undo-btn":
undoChanges();
break;
}
}

另外,也可以通过库进行处理。JQuery库就有处理事件代理的相应函数。

3.阻止事件冒泡

现在我们考虑下这种情况,html代码如下:

1
2
3
<div>
<p>Click me!</P>
</div>

在div标签下,我们需要绑定一个点击事件,该事件用于处理div标签,假设需要输出:这是一个DIV标签。
而在p标签,我们同样需要绑定一个点击事件,该事件用于处理P标签,假设需要输出:这是DIV标签下的P标签。
在正常的事件过程中,当点击P之后,事件会向上冒泡,引起div标签的click事件发生。但是这不是我们所要的效果,我们需要的是点击P标签的时候只执行p标签绑定的点击事件,于div绑定的点击事件无关。这时候就需要用到stopPropagation()函数,它能阻止当前元素的事件向上冒泡。
Example点击查看
Example点击查看
第一个例子中没有阻止事件冒泡,第二个例子中添加了阻止事件冒泡的函数stopPropagation();