关于JS中事件代理的解析

概述

一般来说,我们在为前端页面设计交互的的时候往往需要为DOM元素添加事件处理程序。但是很多时候页面的DOM元素的结构和层级会很复杂,如果我们为所有需要添加事件处理的DOM元素一一绑定上事件处理程序,那么不仅编写出的代码会很繁杂,整个页面的性能也会很低下。比如我们有一个有序或者无序的列表,其中包裹了数百个子节点li,一般来说,在通过选择器拿到元素集合后,我们会用for循环对集合进行遍历,然后为其添加事件处理方法,这么做会带来什么结果呢,在JS引擎中,代码操作的DOM数量和添加的事件处理程序数量直接关系到页面的整体性能。操作的DOM数量越多,对DOM的访问次数越多,浏览器的重绘次数就越来越多,页面的响应速度和运行性能也就越来越差。所以,在进行程序优化的过程,其中一个重要的思路就是减少DOM的操作。

这时,事件代理存在的重要意义就体现出来了,它为我们提供了一种新的解决为大量类似的DOM元素添加事件处理的解决方案。只为一个容器或者说父节点添加一次事件处理程序,就达成了控制这个容器中一系列元素的目标,大大的减少了浏览器对DOM的访问。

事件代理原理

事件代理本质上来说是利用事件冒泡的机制来进行实现的。何为事件冒泡呢,百科中的解释是当事件发生后,这个事件就要开始传播(从里到外或者从外向里)。为什么要传播呢?因为事件源本身(可能)并没有处理事件的能力,即处理事件的函数(方法)并未绑定在该事件源上。例如我们点击一个按钮时,就会产生一个click事件,但这个按钮本身可能不能处理这个事件,事件必须从这个按钮传播出去,从而到达能够处理这个事件的代码中(例如我们给按钮的onclick属性赋一个函数的名字,就是让这个函数去处理该按钮的click事件),或者按钮的父级绑定有事件函数,当该点击事件发生在按钮上,按钮本身并无处理事件函数,则传播到父级去处理。

相关实现

具体怎么实现事件代理呢,我们来看一些简单的相关例子:

先写一个简单的有序列表结构。

<ol>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ol>

比如我们想要实现的是点击li,弹出li的序列。如果要直接操作li。

var lists = document.querySelectorAll('li');
for(let i = 0; i < lists.length; i ++){
    lists[i].onclick =function () {
        alert(lists[i].innerText);
    }
}

通过选择器拿到了li的集合,然后进行遍历并一一绑定点击事件,如果使用事件代理来实现呢

var lists = document.querySelector('ol');
lists.onclick = function (event) {
    alert(event.target.innerText);
}

通过选择器拿到li的容器也就是父节点ol,然后只绑定一次点击事件,并通过event对象提供的属性target指向容器的子节点,然后弹出该子节点的文本内容。需要了解的是,event对象提供了两个属性,分别是currentTarget和target,前者指向的是绑定事件的当前节点,后者则指向当前节点的一级子节点。当我们面临事件冒泡和事件捕获时,这两个属性能够为我们提供更高的灵活度。

上诉例子中,我们需要添加事件的DOM元素都是相同的,事件的种类也是一样的,如果我们面对的是结构更复杂一些的DOM集合呢,来看下面的例子。

DOM结构:

<div class="wrap">
    <button>清空</button>
    <input type="submit" value="提交">
    <input type="text">
    <input type="checkbox">
</div>

在这样的DOM结构中,我们要为四种不同的元素添加事件控制,如果只是通过target指向的话是无法实现的,需要挖掘target更深层的属性来进行条件判断,而target具备的属性就是指向的子节点所具备的属性,比如nodeName、id、className、value等

let box = document.querySelector('.wrap');
box.onclick = function (event) {
    if(event.target.nodeName === 'button'){
        //...
    }else if(event.target.value === '提交'){
        //...
    }else if(event.target.className === 'text'){
        //...
    }else{
        //...
    }
};

这样我们同样只在父节点上绑定一次事件就完成了我们想要的效果,无论是编码效率和性能损耗都比不适用事件代理的情况下更加优化。我们可以发现,相比与遍历元素集合的方式,事件代理最大的好处就是减少了DOM的操作,从来提升了效能。

适用场景

事件代理并非在所有场景中都适用的,在使用事件代理时,我们需要考虑添加事件处理的父节点能否触发我们想要绑定的事件,比如:

<div class="wrap">
    <input type="text"><br>
    <input type="text">
</div>

当我们想为容器中的文本框绑定blur事件改变框体颜色时

let box = document.querySelector('.wrap');
box.addEventListener('blur',function (event) {
    event.target.style.borderColor = 'red';
});

我们发现这样做是无法生效的,应为div元素是无法触发onblur事件的,同时还有其他有关的输入框事件也是无法触发的,如oninput、onfocus等。

相关推荐