* 什么是事件委托
委托,就是让别人帮我们做事。某件事情本身应该由你来做,而你却加到别人身上来完成。事件委托,也叫事件代理,JavaScript高级程序设计上讲:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
为了更好的帮助理解,这里有个很经典的一个例子: 快递员送快递,如果一个快递员送一个公司的快递,他可以选择在公司联系每个人来取这个快递,当然另一种方法就是把快递让前台的MM代收,然后公司的人只要自己来前台取就OK了,虽然结果是一样的,但是效率却变快了许多。
* 为什么要用事件委托
正常情况下,对用户的一次操作进行响应和互动,我们需要对DOM节点(点我深入理解DOM )绑定相应的事件处理程序,执行对DOM的一次访问;那如果我们有很多类似的操作呢?如下图:
你可能会用到for循环
的方法,来遍历每一个item,然后给他们添加事件,这么做是低级程序员的思维。
我们知道,DOM操作本身就是很慢的,在每一次执行DOM操作过程中,浏览器都需要在内存中生成DOM树,如果我们添加了过多的页面处理程序,浏览器就需要不断的与DOM节点进行交互,访问的次数越多,引起浏览器重绘和重排的次数也就越多,直接影响整个页面的就绪时间。我们知道Web APP 和 Native APP相比的痛点就在这里(不知道的朋友,可以猛戳这里)。这也是为什么性能优化主要思想之一就是减少DOM操作的原因。
但是,如果用事件委托,利用事件冒泡到父元素节点【parentNode】,我们将事件处理程序绑定在父元素节点上,此时与DOM交互就只有一次,大大的减少与DOM的交互次数,提高性能;
也许有人问,那什么是事件冒泡?
知道的人直接往下拉,毕竟基础文,讲详细点没坏处。
什么是事件冒泡?我们已经知道HTML DOM的树结构如下,事件冒泡就是从最深层的节点开始,然后逐渐向上传播事件。比如我们给如下元素节点绑定click
事件,当我们点击时,这个事件就会一层一层往外执行。再举个例子,假如我们有这么一个节点树div > ul > li > a
,我们点击a
节点时,因事件冒泡,执行顺序就是a > li > ul > div
。
* 事件委托的原理
因为有事件冒泡这种机制,我们给上例中的 div
添加点击事件,那么,当他的所有子元素节点触发点击事件时,都会冒泡到该div
上,所以,既使子元素上没有绑定点击事件,在用户执行点击操作的时候,也依旧能被触发响应。这就是事件委托,委托他们的父元素节点代为执行。
经典例子中,前台MM就是这个父元素节点,各个员工到前台来领快递就是事件冒泡过程。
特别提醒: 事件委托是通过事件冒泡实现的,所以如果子级的元素阻止了事件冒泡,那么事件委托也将失效!
* 事件委托的实现(核心)
让我们用例子来帮助理解:
html代码
- 111
- 222
- 333
- 444
javascript代码
window.onload = function() {
var parent = document.getElementById("parent");
var sons = document.getElementsByTagName("li");
for (var i = 0; i < sons.length; i++) {
sons[i].onclick = function() {
document.write(sons[i-1].innerHTML)
}
}
}
效果展示
当我们点击任意 li 标签的时候,页面结果都为 444。每次点击,首先要找到ul,然后遍历li,然后点击li的时候,又要找一次目标的li的位置,才能执行最后的操作,每次点击都要找一次li, 找一次li,就执行了一次DOM操作
用事件委托怎么实现?
想想事件委托的原理,就是把事件绑定到父节点上去,那就有了:
html代码
- 111
- 222
- 333
- 444
javascript代码
window.onload = function() {
var parent = document.getElementById("parent");
// var sons = document.getElementsByTagName("li"); // 不需要访问子元素节点了
parent.onclick = function() { // 在父元素节点上触发点击事件
document.write("hello");
}
}
现在,每次点击子元素节点后,先由事件冒泡将事件冒泡到父节点上,再由父元素代为执行。每次点击,只执行了一次DOM操作。
但是,我们观察到,再上个例子每次执行结果都是444,这个例子也不能区分是哪个子元素触发了点击时间,这与我们分别绑定到 li 事件的响应效果不同,并且,点击ul标签,也会触发点击事件。这就不行了,这都搞不定,事件委托就用不了了啊。
还好,Event对象提供了一个属性叫 target:可以返回事件的目标节点,也就是说,target就可以表示为当前的事件操作的dom,但是不是真正操作dom,这个是有兼容性的;标准浏览器用 ev.target,IE浏览器用 event.srcElement。
这里我们用 nodeName 来获取具体是什么标签名,这个返回的是一个大写的,我们需要转成小写再做比较,那我们再看下面的代码
什么?你不知道nodeName是干嘛的?没关系,戳这里。中部位置有介绍各种节点的nodeName的值。什么?你也不知道节点是干嘛的?朋友,建议你把该文通篇看一遍。
用事件委托实现区分点击不同
javascript代码
window.onload = function() {
var parent = document.getElementById("parent");
parent.onclick = function() {
var ev = ev || window.event; // 写全: ev = ev ? ev || window.event; 兼容ie
var target = ev.target || ev.srcElement; // 兼容ie
if (target.nodeName.toLocaleLowerCase() == "li") {
document.write(target.innerHTML);
}
}
}
实现效果
咋样,有没有被帅到。这样改就只有点击
用事件委托实现区分点击不同
相信大家,理解了上一步之后,这个需求也不难,既然target能拿到nodeName,自然也能拿到id:
先看看原始做法:
html代码
javascript代码
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('选择');
}
}
效果展示
理解: 意图很明显,有四个不同操作的四个按钮,点击响应不同的事件,这种方法需要对每个元素节点都执行DOM访问,如果多了,还是慢和卡顿的问题。
究极进化:事件委托
javascript代码
window.onload = function(){
var oBox = document.getElementById("box");
oBox.onclick = function (ev) {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLocaleLowerCase() == 'input'){
switch(target.id){ // target 就是一个元素节点,是被触发的元素节点
case 'add' :
alert('添加');
break;
case 'remove' :
alert('删除');
break;
case 'move' :
alert('移动');
break;
case 'select' :
alert('选择');
break;
}
}
}
}
完美,用事件委托就可以只用一次dom操作就能完成所有的效果。
其实 target 是 被触发事件的目标节点,我们还能获取更多信息,比如
html代码
Hello
我们用target获取
元素的文本节点
if ( target.nodeName.toLowerCase() == "p") {
alert(target.firstChild.nodeValue);
alert(target.childNodes[0].nodeValue);
}
* 事件委托 横向拓展
对新增节点是否生效? - 是
之前讲的都是document加载完成的现有dom节点下的操作,那么如果是新增的节点,新增的节点会有事件吗?也就是说,一个新员工来了,他能收到快递吗?
等等,我们先解决新增一个节点的问题
var newnode = document.createElement("li");
newnode.innerHtml = "new member";
parent.appendChild(newnode);
完美,继续。
来看看原始代码,你是不是这样写:
html代码
- 111
- 222
- 333
- 444
javascript代码
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
很抱歉告诉你,这样新增的节点是不会有我们的onmouseover
和onmouseout
方法的。
执行结果:
新增了555这个子节点之后,移入该节点
意识到错误后,可能会这样改进:将公共方法封装成函数,然后新增的时候调用这个函数:
javascript代码
window.onload = function(){
var oBtn = document.getElementById("btn");
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
var num = 4;
function mHover () {
//鼠标移入变红,移出变白
for(var i=0; i
效果展示
恭喜你,效果实现了,但是,本身DOM操作次数就很多了,这又增加了一次访问,性能上还是不可取的。
还有办法吗?当然有啊,事件委托用起来
看招
html代码
- 111
- 222
- 333
- 444
javascript代码
window.onload = function(){
var oBtn = document.getElementById("btn");
var oUl = document.getElementById("ul1");
var num = 4;
oUl.onmouseover = function () {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if (target.nodeName.toLowerCase() == "li") {
target.style.backgroundColor = "red";
target.style.fontSize = "20px";
}
}
oUl.onmouseout = function () {
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if (target.nodeName.toLowerCase() == "li") {
target.style.backgroundColor = "#fff";
target.style.fontSize = "16px";
}
}
oBtn.onclick = function () {
num++;
var newnode = document.createElement('li');
newnode.innerHTML = 111 * num;
oUl.appendChild(newnode);
}
}
效果展示
可以看到,555的字体和背景颜色都改变了。这种实现方式,已经十分接近原生效果,只产生一次DOM操作。
* 事件委托 纵向拓展
元素节点深度参差不齐,能否处理? - 能
上面的案例都有一个共性,那就是
- 标签下就是
- 了,并且
- 是最小子节点,那么,如果一个列表中,有的
- 还有子节点,有的
- 又没有子节点,那怎么办?
你说怎么拌,我觉得凉拌容易拉肚子,还是热的好吃。看招:
html代码-
11111111111
-
22222222
- 3333333333
- 4444444
javascript代码
window.onload = { var box = document.getElementById("box"); box.addEventListener('click', function(ev) { var target = ev.target; while (target !== box) { if (target.tagName.toLowerCase() == 'li') { alert("li clicked~"); break; } target = target.parentNode; } }); }
效果展示
* 总结
我们可以发现,当用事件委托的时候,根本就不需要去遍历元素的子节点,只需要给父级元素添加事件就好了,其他的都是在js里面的执行,这样可以大大的减少dom操作,这才是事件委托的精髓所在。
最后总结一下:什么事件可以用事件委托,什么样的事件不可以用呢?
- 适合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。值得注意的是,mouseover和mouseout虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。
- 不适合的就有很多了,举个例子,mousemove,每次都要计算它的位置,非常不好把控,在不如说focus,blur之类的,本身就没用冒泡的特性,自然就不能用事件委托了。
-