事件委托也叫事件代理。《JavaScript 高级程序设计》的说法是:利用事件冒泡,只指定一个处理程序,就能管理某一类型的所有事件。网上常用“代收快递”来讲,这个比方确实贴切。
有三个同事周一会收到快递。签收有两种办法:三人站门口等,或委托前台代签。公司不会容忍一堆人堵门口,所以通常选委托。前台判断收件人、按要求签收甚至代付款。新员工来了,寄给他的件前台照样能核实代签。
这里其实有两层意思:现有 DOM 节点有事件;后来新加的 DOM 节点也有事件。
| 做法 |
DOM 交互 |
新增节点 |
| 给每个 li 绑 click |
遍历 + 每次点击再定位 |
要再绑一次,否则没事件 |
| 父级委托 + target 判断 |
只交互一次 |
天然覆盖新节点 |
一、为什么要用事件委托
少量节点直接绑事件没问题。一百个 li 都要同样的 click,用 for 循环逐个添加,会有性能问题:页面上的处理程序数量直接影响整体性能,访问 DOM 越多,重绘重排越多,交互就绪越慢。性能优化的一条主思路就是减少 DOM 操作。
委托把判断放到 JS 里,和 DOM 只交互一次,交互次数大幅下降。
二、实现原理:冒泡
事件从最深节点往上传播。例如 div > ul > li > a,给最里面的 a 加 click,执行顺序是 a → li → ul → div。因此给最外层 div 加点击,里面的 ul、li、a 点击都会冒泡上来,这就是委托:父级代执行。

三、基本操作:同样效果的子节点
先看一般写法。HTML:
<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。改成委托:
window.onload = function(){
var oUl = document.getElementById("ul1");
oUl.onclick = function(){
alert(123);
}
}
li 被点时冒泡到 ul,ul 上有 click 就会触发。但点 ul 空白处也会触发。若只要点 li 才响应,用事件源:target(标准)或 srcElement(IE),再用 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 很多时收益很明显。
四、每个子节点效果不同
若每个按钮行为不同,委托还有用吗?HTML:
<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('选择');
}
}
四个按钮四种操作,至少四次 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;
}
}
}
}
五、新增节点也要有事件
文档加载完的节点好办。新员工来了,他能收到快递吗?先看常规添加:
<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 里完成。这才是精髓。
六、点击落在 li 内部更深的节点
场景: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>
22222222
</div>
</li>
<li>
<span>3333333333</span>
</li>
<li>4444444</li>
</ul>
从 target 往上走到 ul,遇到 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 就是沿冒泡路径往外走,当前 target 是 li 时执行逻辑并结束循环。也可以抽成递归。复制到本地即可看到效果。
七、哪些事件适合委托
适合:click、mousedown、mouseup、keydown、keyup、keypress。
mouseover / mouseout 虽然会冒泡,但经常要算位置,不好处理。mousemove 每次都要算位置,更难控。focus / blur 本身不冒泡,不能委托。
来源:https://www.cnblogs.com/liugang-vip/p/5616484.html
一句话总结:事件委托就是把一类事件交给祖先,用冒泡和 target(必要时向上找 li)判断真正点了谁,从而少碰 DOM,连后来插入的节点也能覆盖。