JavaScript事件委托的技术原理

2,079 阅读5分钟

了解事件委托之前请仔细看封面大图中的泡泡是怎么变化的






事件委托


事件委托就是子级将事件委托给父级,开始监听,如果触发,父级通过ev.target来获取单击的哪个目标,从而弹出目标

为什么要用事件委托:


一般来说,dom需要有事件处理程序,我们都会直接给它设事件处理程序就好了,那如果是很多的dom需要添加事件处理呢?比如我们有100个li,每个li都有相同的click点击事件,可能我们会用for循环的方法,来遍历所有的li,然后给它们添加事件,那这么做会存在什么影响呢?

在Js中,添加到页面上的事件数将直接关系页面的整体运行性能,因为需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘重排的次数也就越多,就会延长整个页面的交互时间,这就是为什么性能优化的主要思想之一就是减少DOM操作的原因.如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能.

每个函数都是一个对象,是对象就会占用内存,对象越多,内存占用率就越大,自然性能就越差了(内存不够了是硬伤),比如上面的100个li,就要占用100个内存空间,如果是1000个,10000个呢,那只能说GG了,如果用事件委托,那么我们就可以只对它的父级(如果只有一个父级)这一个对象进行操作,这样我们就需要一个内存空间就够了,是不是省了很多,100个内存空间和1个内存空间你觉得哪个更好呢。

事件委托的原理:

冒泡


事件委托是利用事件的冒泡原理来实现的,就是事件从最深的节点开始,然后逐步向上传播事件,举个例子:页面上有这么一个节点树,div>body;比如给最里面的a(假设此时div中新增了ul,li,a标签)加一个click点击事件,那么这个事件就会一层一层的往外执行,执行顺序d>li>ul>div,有这样一个机制,那么我们给最外面的div加点击事件,那么里面的ul,li,a做点击事件的时候,都会冒泡到最外层的div上,所以都会触发,这就是事件委托,委托它们父级代为执行事件。

阻止冒泡

w3c是e.stopPropagation(),IE则是e.cancelBubble = true

HTML

<input type="button" name="" id="btn" value="添加" />
<ul id="ul1">
    <li>111</li>
    <li>222</li>
    <li>333</li>
    <li>444</li>
</ul>

JS

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操作,这才是事件委托的精髓所在。


现在给一个场景 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>

上列表,有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的时候,就可以执行对应的事件了,然后终止循环。

页面处理大型数据时

当页面遇到大型数据时,可根据监听window.addEventListener和window.requestAnimationframe()来处理数据

具体可借鉴一下传送门

页面加载海量数据

如上述文章中有不对的地方,可提取出来,及时发于评论我将积极改正。