天天看点

深入理解js中的事件委托

事件委托是前端面试的经典面试题型,上次面试给我整的一脸懵逼,好尴尬。准备找工作的小伙伴赶紧学习学习,也许会对你有帮助。

事件委托原理

事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
(也就是说通过父元素监听子元素触发的事件)
           

为什么使用事件委托

在js中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的与dom节点进行交互,我们知道,频繁的操作dom是很影响性能的,如果使用事件委托,可以大大减少与dom的交互次数,从而提高性能。
           

如何使用事件委托

下面用实例来介绍事件委托的使用方法。
           

1、子节点实现相同的功能。(点击li,弹出123)

<ul id="ul1">
    <li>111</li>
    <li>222</li>
    <li>333</li>
    <li>444</li>
</ul>
           

一般方法:

window.onload = function(){
     var oUl = document.getElementById('ul1');
     var aLi = oUl.getElementsByTagName('li');
     for(var i=0; i<aLi.length; i++){
         aLi[i].onclick = function(){
             alert(123);
         }
     }
 }
           

相信大多数人都这么实现功能,这里操作了多少次dom呢,首先要找到ul,然后遍历li,然后点击li的时候,又要找一次目标li的位置,才能执行最后的操作。每次点击都要找一次li。

下面看看用事件委托的方式:

window.onload = function(){
     var oUl = document.getElementById('ul1');
     var aLi = oUl.getElementsByTagName('li');
     oUl.onclick = function(){
	     alert(123);
     }
 }
           

用父级ul做事件处理,当li被点击时,由于冒泡原理,事件就会冒泡到ul上,然后出发事件。但是这里点击ul的时候,也是会触发的。这要怎么解决呢?

Event对象提供了一个属性叫target,可以返回事件的目标节点,称之为事件源。即target就可以表示为当前的事件操作的dom,但是不是真正操作dom, 这个是有兼容性的,标准浏览器用ev.target,IE浏览器用event.srcElement,此时只是获取了当前节点的位置,并不知道是什么节点名称,这里我们用nodeName来获取具体是什么标签名,这个返回的是一个大写的,我们可以转成小写再做比较

oUl.onclick = function(e){
    var e = e || window.event;
    var target = e.target || ev.srcElement;
    console.log(e)
    console.log(target.nodeName)
    if(target.nodeName.toLowerCase() == 'li'){
        alert(123);
    }
}
           

这要问题就解决啦!而且每次只执行一次dom操作,如果li数量很多的话,将会大大减少dom的操作,优化性能。

2、子节点实现不同的功能。

还是上代码

<div id="box">
     <input type="button" value="添加" id="add">
     <input type="button" value="删除" id="remove">
     <input type="button" value="移动" id="move">
     <input type="button" value="选择" id="select">
 </div>
           
window.onload = function(){
	/* 普通方式 */
     var oUl = document.getElementById('ul1');
     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("选择");
     }

	 /* 事件委托*/
	var oBox = document.getElementById('box');
	oBox.onclick = function(e){
		var e = e || window.event;
		var target = e.target || e.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;
		         
		     }
		}
	     
	
	 }
 }    
       
           

3、元素添加新的子节点

<input type="button" name="" id="btn" value="添加" />
<ul id="ul1">
    <li>111</li>
    <li>222</li>
    <li>333</li>
    <li>444</li>
</ul>
           

要实现移入li,li变红,移除li,li变白的效果。

window.onload = function(){
    var oBtn = document.getElementById("btn");
    var oUl = document.getElementById("ul1");
    var aLi = oUl.getElementsByTagName('li');
    var num = 4;
    
    //鼠标移入变红,移出变白
    for(var i=0; i<aLi.length;i++){
        aLi[i].onmouseover = function(){
            this.style.background = 'red';
        };
        aLi[i].onmouseout = function(){
            this.style.background = '#fff';
        }
    }
    //添加新节点
    oBtn.onclick = function(){
        num++;
        var oLi = document.createElement('li');
        oLi.innerHTML = 111*num;
        oUl.appendChild(oLi);
    };
}
           

结果发现,新增的li是没有事件的,说明添加子节点的时候,事件是没有一起添加上。那么解决方法可以讲for循环用一个函数包起来,如下:

window.onload = function(){
    var oBtn = document.getElementById("btn");
    var oUl2 = document.getElementById("ul2");
    var aLis= oUl2 .getElementsByTagName('li');
    var num = 4;
	function mHover(){
	    //鼠标移入变红,移除变白
	    for(var i=0; i<aLis.length; i++){
	        aLis[i].onmouseover = function(){
	            this.style.background = 'red';
	        }
	
	        aLis[i].onmouseout = function(){
	            this.style.background = 'none';
	        }
	    }
	
	}
	mHover();

	 //添加新节点
	 oBtn.onclick = function(){
	    num++;
	    var oLi = document.createElement('li');
	    oLi.innerHTML = 111*num;
	    oUl2.appendChild(oLi);
	    mHover();
	}
} 
           

这样虽然达到目的了,但是实际上无疑又是增加了个dom操作,在优化性能方面是不可取的,再来看看事件委托的方式如何:

window.onload = function(){
    var oBtn = document.getElementById("btn");
    var oUl2= document.getElementById("ul2");
    var num = 4;
	oUl2.onmouseover = function(e){
	   var e = e || window.event;
	    var target = e.target || e.srcElement;
	    if(target.nodeName.toLowerCase() == 'li'){
	        target.style.background = 'red';
	    }
	}
	oUl2.onmouseout = function(e){
	    var e = e || window.event;
	    var target = e.target || e.srcElement;
	    if(target.nodeName.toLowerCase() == 'li'){
	        target.style.background = 'none';
	    }
	}
	//添加新节点
	oBtn.onclick = function(){
	    num++;
	    var oLi = document.createElement('li');
	    oLi.innerHTML = 111*num;
	    oUl2.appendChild(oLi);
	}
}
           

这就实现了同样的效果,而且这种方式根本不需要遍历元素的子节点,只需要给父元素添加事件就好了,其他的都是在js里面执行,大大减少了dom操作,这才是事件委托的精髓所在。

最后

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

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

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