概述:
什么叫事件委托?他的另一个名字叫事件代理。《JavaScript高级程序设计》13.5章节对其进行了详细描述:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
举个栗子:取快递。有三位同事预计会在今天收到各自的快递,为了签收,有两种方法:1.三个人站在门口等着,2.委托给前台MM代收。现实中,我们都会采用第二种方式,前台MM收到快递后,判断是谁的快递,是否需要到付,另外,即使是公司新来的员工,前台MM也可以代为签收。
这里有两层意思:
第一,大家可以通过委托前台MM代收快递,即程序中现有DOM节点是有事件的;
第二,新员工也可以通过前台MM代收快递,即程序中新添加的DOM节点也是有事件的。
为什么使用事件委托:
一般来说,DOM需要有事件处理程序,我们直接给他设置就行,但是如果很多DOM需要时间处理程序呢?假如有100个 li 需要添加 click 事件,我们可以用 for 循环遍历所有 li 元素,给他们添加事件,但是这样做好吗?
在 JavaScript 中,添加到页面上的事件处理程序数量将会直接影响到页面的整体运行性能,因为需要不断与DOM节点进行交互,访问DOM次数越多,引起的浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这也是为什么性能优化的主要思想之一就是要减少对DOM的操作。如果使用事件委托,则只需要一次DOM交互即可,这样就能大大提升性能。
我们都知道,每个函数都是一个对象,都会占用内存,对象越多,占用内存就越大。如果使用事件委托,那么我们就可以只对他的父级这一个对象进行操作,大大节省了内存,提升了性能。
事件委托的原理:
首先要知道,事件委托是利用事件冒泡实现的,什么是事件冒泡?《JavaScript高级程序设计》13.1章有详细介绍。简单说,事件是从DOM树的最深节点开始,逐层向上传播事件。举个栗子:页面上有这么一个节点树,div>ul>li>a,此时给 a 添加一个 click 事件,那么就会一层一层往上执行,假如刚好最外层的 div 也有一个 click 事件,那么 div 的事件也会被触发,这就是事件冒泡。
事件委托的实现方法:
到重点部分了,我们先来看一段一般方法的例子:
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
实现功能是点击 li ,弹出 hello world
window.onload = function(){
var dom_ul = document.getElementById('ul');
var dom_lis = document.getElementsByTagName('li');
for(var i-0;i<dom_lis.kenght;i++){
dom_lis[i].onclick = function(){
alert('hello world');
}
}
}
上面的代码很简单,相信很多人都是这样做的。分析一下,看看有多少次的DOM操作:首先找到 ul,然后遍历 li ,点击 li 的时候,又要找一次目标的 li 的位置,才能执行最后的操作。
使用事件委托又会是怎么样呢?
window.onload = function(){
var dom_ul = document.getElementById("ul");
dom_ul .onclick = function(){
alert('hello world');
}
}
这里用父级元素 ul 做事件处理,当 li 被点击时,由于冒泡原理,事件会冒泡到 ul 上,因为 ul 上有点击事件,所以事件会被触发,当然了,这里点击 ul 的时候,也是会触发的,那么问题来了,如果我们想让事件代理的效果跟直接给节点的事件效果一样怎么办,比如说只有点击 li 时才会触发。
Event 对象提供了一个属性,叫做 target ,可以返回事件的目标节点,也就是事件源,target 就可以表示为当前事件操作的DOM,但是并不需要去操作DOM。但是,这个有兼容性问题,标准浏览器采用 Event.target,IE浏览器采用 Event.srcElement。此时我们获取到了当前节点的位置,但是并不知道是什么节点,我们用 nodeName 来获取名称,返回值是大写的,需要转换为小写,如下:
window.onload = function(){
var dom_ul = document.getElementById('ul');
dom_ul.onclick = function(event){
var event = event || window.event;
var target = event.target || event.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
alert('hello world');
}
}
}
这样的话,就只有点击 li 才会触发事件了,且每次只执行一次DOM操作,大大提升了性能。
上面的例子是在 li 的操作相同情况下的,若是每个 li 的点击效果不同呢?
<div id="box">
<input type="button" id="add" value="添加" />
<input type="button" id="remove" value="删除" />
<input type="button" id="move" value="移动" />
<input type="button" id="select" value="选择" />
</div>
window.onload = function(){
var Add = document.getElementById('add');
var Remove = document.getElementById('remove');
var Move = document.getElementById('move');
var Select = document.getElementById('select');
Add.onclick = function(){
alert('添加');
}
Remove.onclick = function(){
alert('删除');
}
Move.onclick = function(){
alert('移动');
}
Select.onclick = function(){
alert('选择');
}
}
怎样进行事件委托呢?
window.onload = function () {
var Box = document.getElementById('box');
Box.onclick = function (ev) {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if (target.nodeName.toLowerCase() == 'input') {
switch (target.id) {
case 'add':
alert('添加');
break;
case 'remove':
alert('删除');
break;
case 'move':
alert('移动');
break;
case 'select':
alert('选择');
break;
}
}
}
}
新情况又出现了,来了个新员工,他能收到快递吗?
正常情况下添加节点元素:
<input type="button" name="" id="btn" value="添加" />
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
</ul>
期望效果是:鼠标移入 li ,li 变红,鼠标移出 li , li 变白,点击“添加”按钮可以向 ul 中添加一个 li
window.onload = function () {
var Btn = document.getElementById('btn');
var ul = document.getElementById('ul');
var lis = document.getElementsByTagName('li');
var num = 4;
//鼠标移入变红,移出变白
for (var i = 0; i < lis.length; i++) {
lis[i].onmouseover = function(){
this.style.background = 'red';
};
lis[i].onmouseout = function(){
this.style.background = 'white';
}
}
//添加新节点
Btn.onclick = function(){
num++;
var li = document.createElement('li');
li.innerHTML = num;
}
}
这是一般的做法,你会发现新增的 li 是没有事件的,使用事件委托可以吗?
答案当然是可以,看看下面优化后的做法:
window.onload = function(){
var Btn = document.getElementById('btn');
var ul = document.getElementById('ul');
var lis = document.getElementsByTagName('li');
var num = 4;
//事件委托,添加的子元素也有事件
ul.onmouseover = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
target.style.background = 'red';
}
}
ul.onmouseout = function(){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
target.style.background = 'white';
}
}
Btn.onclick = function(){
num++;
var li = document.createElement('li');
li.innerHTML = num;
ul.appendChild(li);
}
}
采用事件委托的方式,我们不需要去遍历子元素,只需要给父元素添加事件即可,并且新生成的元素也是有事件的。这就是事件委托的精髓所在。
还有一种情况,假如结构是这样的 div>ul>li>a,不同点在于ul,li,a都是有宽高,并且和 div 是一样大的,此时还是给 ul 绑定事件,那么 target 就有可能是 li ,也有可能是 a ,如何做区分呢?
var ul = document.getElementById("ul");
ul.addEventListener("click", function(ev) {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
while (target.nodeName.toLowerCase() !== "ul") {
if (target.nodeName.toLowerCase() == "li") {
alert("hello world");
break;
}
target = target.parentNode;
}
});
核心代码是 while 循环部分,实际上是一个递归调用,也可以携程一个函数,用递归方法来调用,用冒泡原理,从里往外冒,直到达到当前位置,当此时的 target 是 li 的时候,就可以执行对应的事件了,然后终止循环。
总结:
适合使用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。
不适合的就很多了,mousemove,每次都要计算他的位置,很难把控,focus,blur之类的,本身就没有冒泡属性,自然也就不能用事件委托了。