js中的事件委托或事件代理详解

    |     2020年4月18日   |   web前端技术, 优化   |     0 条评论   |    1083

事件委托也叫事件代理。《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 点击都会冒泡上来,这就是委托:父级代执行。

事件委托1

三、基本操作:同样效果的子节点

先看一般写法。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,连后来插入的节点也能覆盖。

转载请注明来源:js中的事件委托或事件代理详解
本文链接地址:https://ai.zhousir.top/?p=2857
回复 取消