事件委托,又叫事件代理。
一般来说,如果标签需要添加事件,我们都会直接给它添加事件处理程序就好了。
let ul= document.getElementById("ul");
let myFun = function(event){
console.info( "You click me" );
};
ul.addEventListener("click",myFun);
但是,如果是很多的标签需要添加事件处理呢?比如我们有 100 个 li,每个li都有相同的click点击事件,可能我们会用for循环的方法,来遍历所有的 li,然后给它们添加事件。
如果为每个Li都添加事件,则会造成DOM访问次数过多,引起浏览器重绘与重排的次数过多,性能则会降低。
- 111
- 222
- 333
- 444
上面的代码很简单,整体思路就是:
首先要找到ul;
然后遍历li ,依次给 li 添加事件;
然后点击li的时候,又要找一次目标的li的位置,才能执行最后的操作,每次点击都要找一次li。
在 JS 中性能优化的其中一个主要思想是减少DOM操作。
用事件委托的方式来实现,可以实现减少DOM操作的优化。
实现事件委托是利用了事件的冒泡原理实现的。
当我们为最外层的节点添加点击事件,那么里面的ul、li、a等标签的点击事件都会冒泡到最外层节点上,委托它代为执行事件。
为了避免标签层级过多,一般都把事件委托到父标签上。这叫“就近委托”。
let ul= document.getElementById("ul");
let lies= ul.getElementsByTagName('li');
let myFun = function(event){
console.info( lies.length );
};
ul.addEventListener("click",myFun);
用父级ul做事件处理,当li被点击时,由于冒泡原理,事件就会冒泡到ul上,因为ul上有点击事件,所以事件就会触发。
关于事件冒泡和捕获,大家可以看这篇文章。JavaScript事件捕获与冒泡
当然,当点击 ul 内部空白的地方(这时就是单纯的点击 ul ),也是会触发事件的。
那么问题就来了,如果我只想点击 li 才会触发该事件,该怎么做呢?要知道这个事件是添加在 ul 上的。
这时,Event对象就派上用场了,Event提供了一个属性叫target,可以返回触发此事件的元素,我们称为事件源。
event.target 属性只是获取了当前节点,并不知道是什么节点名称,这里我们可以用 nodeName 来获取具体是什么标签名,这个返回的是一个大写的节点名,我们可以转成小写再做比较,或者不转。
let ul= document.getElementById("ul");
let myFun = function(event){
let ev = event ; // 事件对象
let target = ev.target ;
if(target.nodeName.toLowerCase() === 'li'){
console.info( target.innerHTML );
}
};
ul.addEventListener("click",myFun);
这里只是判断了标签的 nodeName,在实际的应用中,可以继续判断 className 之类的。
如果 ul 下的 li 是动态添加的,可以继续委托事件不呢?
改进下代码。
- 111
- 222
- 333
- 444
点击 btn 新增了 li 标签后,新增的 li 标签 依然有点击事件。说明,事件委托是可以对动态新增的标签添加事件的。
事件委托的优势:
减少事件注册,节省内存,如
table可以代理所有 td 的 click 事件
ul 可以代理所有 li 的 click 事件
减少了 DOM 节点的更新操作,处理逻辑只用委托在父元素上即可,如:
新增的 li 不用绑定事件
删除 li 时,不需要进行元素与处理函数的解绑
事件委托的缺点
事件的委托基于冒泡,对于 onfocus 和 onblur 事件不支持
标签层级过多,冒泡过程中,可能会被某层阻止掉(建议就近委托)
只要事件不支持冒泡或者中途有 event.stopPropagation() 等,那么委托就会失败,所以并不适用于直接在document上进行委托。
补充:jQuery 中的事件委托:
$.on()
$(".parent").on("click","a",function(){
...
});
它是 .parent 元素下的 a 元素事件委托到 $(".parent") 之上,只要在 .parent 元素上有点击,就会自动寻找到该元素下的a元素,然后响应事件。