JS的事件冒泡和事件代理

事件是DOM操作中很重要的一环,当事件发生在某个DOM节点上时,事件在DOM结构中进行一级一级的传递,便形成“流”,事件流就是描述从页面接受事件的顺序。若是能学好事件,基本就可以坐地飞升了。

DOM事件流

关于事件流的理解,《JS高级程序设计》上有个形象的比喻:

可以想象画在一张纸上的一组同心圆,如果你把手指放在圆心上,那么你的手指指向的其实不是一个圆,而是纸上所有的圆。换句话说,在单击按钮的同时,你也单击了按钮的容器元素,甚至也单击了整个页面。

DOM2级事件中规定的事件流包括三个阶段:捕获阶段,目标阶段,冒泡阶段。
如图所示:
avatar

捕获阶段

捕获阶段既是事件开始从根节点流向目标对象的阶段,我们可以在这一阶段对事件进行拦截。
在DOM2级事件规范中,要求事件从document对象开始传递,但是想Chrome和Firfox等主流浏览器却都是从window开始传递。

addEventListener方法的第三个参数是可选的布尔值,可以自行指定事件处理程序是在捕获阶段还是冒泡阶段运行。当值为true时,事件处理程序将在捕获阶段运行。

利用addEventListener()举个简单的栗子。

1
2
3
<div class="box">
<button id="btn">Click Me!</button>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
document.querySelector("#btn").addEventListener("click", function(){
console.log("Button was clicked");
}, true);

document.querySelector(".box").addEventListener("click", function(){
console.log("Div.box was clicked");
}, true);

document.querySelector("body").addEventListener("click",function(){
console.log("Body was clicked");
},true);

document.addEventListener("click",function(){
console.log("Document was clicked");
},true);

window.addEventListener("click",function(){
console.log("Window was clicked");
},true);

点击Clike ME按钮后,输出结果如下:

Window was clicked
Document was clicked
Body was clicked
Div.box was clicked
Button was clicked

可以很明显的看出,在捕获阶段,事件由window对象开始,一级一级向下传递,直到最具体的button对象上。

目标阶段

当事件到达目标节点时,即上栗中的button对象上时,事件就进入了目标阶段,事件在目标节点上被触发,然后逆向回流,直到传播到最外层的文档节点(冒泡阶段).

冒泡阶段

事件在目标事件上触发后,并不在这个元素上终止。它会随着DOM树一层层向上冒泡,直到到达最外层的根节点。也就是说,同一事件会一次在目标节点的父节点,父节点的父节点…直到最外层的节点上触发,addEventListener()方法默认就是从冒泡阶段执行事件处理程序。

继续刚才的小栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
document.querySelector('#btn').addEventListener('click', function () {
console.log("btn was clicked");
});

document.querySelector('body').addEventListener('click', function () {
console.log("body was clicked");
});

document.querySelector('.box').addEventListener('click', function () {
console.log("box was clicked");
});

document.addEventListener('click', function () {
console.log("document was clicked");
});

window.addEventListener('click', function () {
console.log("window was clicked");
});

控制台是这样显示的:

btn was clicked
capture.html:43 box was clicked
capture.html:39 body was clicked
capture.html:47 document was clicked
capture.html:51 window was clicked

为了更清楚的说明问题,我们来做个小demo。

这是个简单的HTML结构:

1
2
3
<div id="parent" data-id="444">
<div id="child" data-id="555"></div>
</div>

为这个HTML稍稍修整一下:

1
2
3
4
5
6
7
8
9
10
11
12
#parent{
background-color: #000;
height: 400px;
width: 400px;
}
#child{
background-color: #fff;
height: 200px;
width: 200px;
margin-left: auto;
margin-right: auto;
}

再加点JS就可以了:

1
2
3
4
5
6
document.getElementById('parent').onclick = function(){
console.log(this.getAttribute('data-id'));
};
document.getElementById('child').onclick = function(){
console.log(this.getAttribute('data-id'));
};

控制台是这么反应的:

555
444

当点击子元素(即白色部分)的时候,父元素的click事件也被触发了。为嘛?这是为嘛?接下来在探究一下另一种可能:不为子元素添加onclick是不是也能冒泡。JS如下:

1
2
3
4
5
6
document.getElementById('parent').onclick = function(){
console.log(this.getAttribute('data-id'));
};
/*document.getElementById('child').onclick = function(){
console.log(this.getAttribute('data-id'));
};*/

控制台反应如下:

444

结果发现,在子元素没有绑定onclick事件的情况下,点击子元素仍然会触发父元素的onclick事件。

接下来再看另一种可能,如果子元素和父元素绑定不同的事件,是否依然能冒泡?
JS如下:

1
2
3
4
5
6
document.getElementById('parent').onkeydown = function(){
console.log(this.getAttribute('data-id'));
};
document.getElementById('parent').onclick = function(){
console.log(this.getAttribute('data-id'));
};

控制台告诉我它终于不冒了,世界终于清净了。

555

但是请注意,click是鼠标的点击事件,依旧会触发同为鼠标事件的mousedown和mouseup的冒泡,呵呵。

阻止事件冒泡

当然,这么一个大杀器我们不能放任它为祸人间,那么怎么阻止事件冒泡呢?很简单,在事件触发时,会传入一个相应的event对象,其中有一个stopPropagation()方法可以阻止事件冒泡(IE中为cancleBubble=true),以下是详细代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
document.getElementById('parent').onclick = function(){
console.log(this.getAttribute('data-id'));
};

document.getElementById('child').onclick = function(ev){
var e = ev || window.event; //IE中的event可以通过window.event随时取到
console.log(this.getAttribute('data-id'));

stopPropagation(e);
};

function stopPropagation(e){
if(e.stopPropagation){
e.stopPropagation();
}else{
e.cancelBubble = true; //兼容IE的写法
}
}

于是,冒泡被成功的阻止了。

事件委托

事件委托

凡事都有两面性,事件冒泡虽然比较烦人,但同时给我们带来了事件委托这一减少DOM操作的神器。
事件委托又叫事件代理,是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。

在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少DOM操作的原因;如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能;

下面我们在用一个小栗子来解释一下事件委托:

1
2
3
4
5
6
7
<ul id="parentUl">
<li>我还是个孩子1</li>
<li>我还是个孩子2</li>
<li>我还是个孩子3</li>
<li>我还是个孩子4</li>
<li>我还是个孩子5</li>
</ul>

1
2
3
4
5
6
7
var ul = document.getElementById("parentUl");
var aLi = ul.getElementsByTagName("li");
for(var i = 0; i < aLi.length; i++){
aLi[i].onclick = function(){
console.log(this.innerHTML);
}
}

点击相应的li,会在控制台中输出li的文本内容。
这种方式来添加事件固然简单,但是需要多次操作DOM,如果有100、1000个同级的元素需要添加事件,这种方式简直不忍直视,而且当我们动态添加新的Li元素的时候,新添加的Li元素是没有被绑定事件的。
当然用以下方法可以解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var ul = document.getElementById("parentUl");
var aLi = ul.getElementsByTagName("li");
showMe();
function showMe(){
for(var i = 0; i < aLi.length; i++){
aLi[i].onclick = function(){
console.log(this.innerHTML);
}
}

}
addNewChild();
function addNewChild(){
var li = document.createElement("li");
li.innerHTML = "你好,我是你的新邻居老王。"
ul.appendChild(li);
showMe();
}

这样确实解决了问题但是又增加了操作DOM的次数,大大降低了性能,让我们来看一下通过事件委托是怎样降低DOM操作次数的的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//开挂版
var ul = document.getElementById("parentUl");
ul.onclick = function(event){
var e = event || window.event;
var source = e.target ||e.srcElement; //target表示在事件冒泡中触发事件的源元素,在IE中是e.srcElement;
if(source.nodeName.toLowerCase() == "li"){
console.log(source.innerHTML);
}
stopPropagation(); //阻止继续冒泡
}

addNewChild();
function addNewChild(){
var li = document.createElement("li");
li.innerHTML = "你好,我是你的新邻居老王。"
ul.appendChild(li);
}

完美!

总结

那什么样的事件可以用事件委托,什么样的事件不可以用呢?

适合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。

值得注意的是,mouseover和mouseout虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。

不适合的就有很多了,举个例子,mousemove,每次都要计算它的位置,非常不好把控,在不如说focus,blur之类的,本身就没用冒泡的特性,自然就不能用事件委托了。


请前往此处查看文中例子的源码



参考资料:

  1. js中的事件委托或是事件代理详解
  2. 关于JS事件冒泡与JS事件代理(事件委托)
  3. DOM事件流与事件委托