本文深入解析JavaScript中的事件委托(代理)机制,阐述其基于事件冒泡的核心原理。通过对比传统遍历绑定与委托模式的性能差异,说明如何减少DOM操作、降低内存占用。文章涵盖动态添加节点的事件处理、复杂嵌套结构的节点判断以及兼容IE浏览器的实现细节,帮助开发者掌握高性能前端交互技巧。
事件委托概述与原理
背景说明
1、这是前端面试的经典题型,要去找工作的小伙伴看看还是有帮助的;
2、其实我一直都没弄明白,写这个一是为了备忘,二是给其他的知其然不知其所以然的小伙伴们以参考;
概述说明
所谓事件委托,亦称事件代理。根据《JavaScript高级程序设计》的定义,事件委托利用事件冒泡机制,仅需指定一个事件处理程序,即可管理某一类型的所有事件。这具体指什么?网络上常用“取快递”的例子来解释,经过仔细揣摩,该比喻十分贴切。此处直接引用该案例以阐明事件委托的原理:
假设三位同事预计周一收到快递。签收方式有两种:一是个别在公司门口等待;二是委托前台代为签收。现实中多采用后者,因为公司不会允许大量员工聚集在门口。前台收到快递后,会核实收件人身份并代为签收,甚至代付。此方案的优势在于,即便公司有新员工入职,前台也能在收到寄给新员工的快递后核实并代为签收。
此处包含两层含义:
第一,现有委托的前台同事可代为签收,即程序中现有的DOM节点已具备事件处理能力;
第二,新员工也可被前台代为签收,即程序中动态新增的DOM节点同样具备事件处理能力。
采用事件委托的原因
采用事件委托的原因
通常,若DOM节点需要事件处理,我们会直接为其设置事件处理程序。但当大量DOM节点需添加相同事件时(例如100个li元素均需click点击事件),若采用for循环遍历并逐个绑定,会产生何种影响?
JavaScript中,页面添加的事件处理程序数量直接关乎整体运行性能。不断与DOM节点交互,访问DOM次数越多,引发浏览器重绘与重排的次数也越多,从而延长页面交互就绪时间。因此,性能优化的核心思想之一是减少DOM操作。若使用事件委托,可将所有操作集中至JS程序中,与DOM的交互仅需一次,从而大幅减少交互次数,提升性能;
每个函数均为对象,对象占用内存,对象数量越多,内存占用率越高,性能自然越差(内存不足是硬伤)。例如上述100个li需占用100个内存空间,若增至1000或10000个,后果不堪设想。若采用事件委托,仅需对父级(假设仅有一个父级)这一对象进行操作,只需一个内存空间即可,节省大量资源,性能自然更优。
事件委托的核心原理
事件委托基于事件冒泡原理实现。所谓事件冒泡,即事件从最深节点开始,逐步向上传播。举例:页面节点树结构为div>ul>li>a。若给最内层的a添加click点击事件,该事件将层层向外执行,顺序为a>li>ul>div。基于此机制,若给最外层的div添加点击事件,当内部ul、li、a发生点击时,事件均会冒泡至外层div,从而触发执行。这就是事件委托,即委托父级代为执行事件。
事件委托的实现方法
事件委托的实现方法
本文核心部分。在介绍具体方法前,先观察一般方法的示例:
子节点实现相同功能
<ul id="ul1">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
实现点击li弹出123的功能:
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");
oUl.onclick = function(){
alert(123);
}
}
此处利用父级ul作为事件处理者。当li被点击时,因冒泡原理,事件将冒泡至ul。由于ul上绑定了点击事件,事件将被触发。当然,直接点击ul也会触发。若希望事件代理效果与直接绑定节点一致(即仅点击li时触发),该如何处理?我们有解决方案:
Event对象提供target属性,返回事件的目标节点,即事件源。target表示当前事件操作的DOM节点,但并非真正操作DOM。此属性具有兼容性,标准浏览器使用ev.target,IE浏览器使用event.srcElement。此时仅获取节点位置,需通过nodeName获取具体标签名,返回值为大写,需转换为小写后再比较(出于习惯):
window.onload = function(){
var oUl = document.getElementById("ul1");
oUl.onclick = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
alert(123);
alert(target.innerHTML);
}
}
}
如此修改后,仅点击li会触发事件,且每次仅执行一次DOM操作。若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('选择');
}
}
上述实现效果不再赘述,简单而言,4个按钮,点击每个执行不同操作,至少需4次DOM操作。若采用事件委托,能否优化?
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){
case 'add' :
alert('添加');
break;
case 'remove' :
alert('删除');
break;
case 'move' :
alert('移动');
break;
case 'select' :
alert('选择');
break;
}
}
}
}
采用事件委托仅需一次DOM操作即可完成所有效果,性能优于上述方式。
动态节点的事件处理
前述内容均针对document加载完成后现有DOM节点的操作。若新增节点,新增节点是否具备事件?即新员工入职后,能否收到快递?
查看常规添加节点的方法:
<input type="button" name="" id="btn" value="添加" />
<ul id="ul1">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
此处实现li移入变红、移出变白效果,点击按钮可向ul中添加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循环封装为函数,命名为mHover,如下:
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<aLi.length;i++){
aLi[i].onmouseover = function(){
this.style.background = 'red';
};
aLi[i].onmouseout = function(){
this.style.background = '#fff';
}
}
}
mHover ();
//添加新节点
oBtn.onclick = function(){
num++;
var oLi = document.createElement('li');
oLi.innerHTML = 111*num;
oUl.appendChild(oLi);
mHover ();
};
}
虽然功能实现且看似良好,但实际增加了DOM操作,在性能优化方面不可取。若采用事件委托方式,能否优化?
window.onload = function(){
var oBtn = document.getElementById("btn");
var oUl = document.getElementById("ul1");
var aLi = oUl.getElementsByTagName('li');
var num = 4;
//事件委托,添加的子元素也有事件
oUl.onmouseover = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
target.style.background = "red";
}
};
oUl.onmouseout = function(ev){
var ev = ev || window.event;
var target = ev.target || ev.srcElement;
if(target.nodeName.toLowerCase() == 'li'){
target.style.background = "#fff";
}
};
//添加新节点
oBtn.onclick = function(){
num++;
var oLi = document.createElement('li');
oLi.innerHTML = 111*num;
oUl.appendChild(oLi);
};
}
如上所示,采用事件委托方式,新添加的子元素具备事件效果。我们发现,使用事件委托时,无需遍历子节点,仅需给父级元素添加事件即可,其余操作均在JS中执行,大幅减少DOM操作,这正是事件委托的精髓所在。
--------------------------------------------------华丽的分割线-------------- -----------------------------------------------------------------------------------------------------
复杂嵌套结构的处理方案
在此感谢@苍茫大地NV 的提问,问题非常棒!👏👏👏
其问题如下:
现给定场景ul > li > div > p,div占满li,p占满div。仍给ul绑定事件,需判断点击的是否为li(假设li内部结构不固定),此时e.target可能是p,也可能是div。此种情况如何处理?
现重现其给定场景
<ul id="test">
<li>
<p>11111111111</p>
</li>
<li>
<div>
</div>
</li>
<li>
<span>3333333333</span>
</li>
<li>4444444</li>
</ul>
如上列表,有4个li,内部内容各异。点击li时,event对象即为当前点击对象。如何指定至li上?此处直接给出解决方案:
var oUl = document.getElementById('test');
oUl.addEventListener('click',function(ev){
var target = ev.target;
while(target !== oUl ){
if(target.tagName.toLowerCase() == 'li'){
console.log('li click~');
break;
}
target = target.parentNode;
}
})
核心代码为while循环部分,实质为递归调用,亦可写成函数递归调用,同时利用冒泡原理,从内向外冒泡,直至currentTarget。当当前target为li时,执行对应事件,然后终止循环,逻辑无误!
此处无法查看效果,大家可以复制过去运行一下!
--------------------------------------------------------------------华丽的分割线----------------------------------------------------------------
适用事件类型总结
总结归纳
何种事件适合使用事件委托,何种不适合?
适合事件委托的事件包括:click,mousedown,mouseup,keydown,keyup,keypress。
值得注意的是,mouseover和mouseout虽具备事件冒泡,但处理时需特别注意,因需频繁计算位置,处理起来较为不易。
不适合的事件较多。例如mousemove,每次均需计算位置,极难把控。又如focus、blur等,本身无冒泡特性,自然无法使用事件委托。
今日内容至此。下次计划介绍事件绑定,欢迎大家关注和阅读。以上纯属个人见解,如有不当之处,万望指正,不胜感谢!