关于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等。