浪费资源和影响性能

在通常情况下,我们给一组DOM节点添加相同的事件,都是使用循环绑定

  1. Array.from(document.querySelectorAll('li'))
  2. .map(li => li.addEventListener('click', handler));

当节点数量比较少的时候,这是最简单直接的方法,没有问题。但是当节点数量达到一定数量级的时候(数千甚至上万,比如表格应用),给这么多个节点都循环绑定相同一个事件,显然会造成资源浪费,而且影响性能。

为什么说浪费资源和性能呢?

首先,在初始化页面的时候,我们先要对这些节点进行一次遍历,DOM节点(也就是HTMLElement)是一个很大很庞杂的对象,遍历DOM节点要比遍历普通数组更耗时。如下图:
事件委托 - 图1

这只是一个简单的只有文本子节点的li,这也说明了为什么使用虚拟DOM会更快。

第二,在我们遍历节点的同时,我们还要给每个节i点添加事件,也就是给节点(或者其proto)添加二级DOM事件。

另外,大家都知道,为每个DOM节点绑定事件后都会有一个event对象被当做参数传入事件函数里面,event对象包含了当前事件发生的信息,这个event对象也是一个很庞大的对象

事件委托 - 图2

所以,总结起来,大量DOM节点的事件绑定造成资源浪费和性能损失原因主要有三点:

  1. DOM节点循环的耗时
  2. 给每个DOM节点添加事件
  3. 每个event对象的创建

事件委托

在了解事件委托之前,我们首先要知道浏览器规定的概念:

DOM2 级事件规定的事件流包括三个阶段:事件捕获目标阶段事件冒泡

  • 事件流:描述的是从页面中接收事件的顺序
  • 事件冒泡:事件开始由最具体的元素接收,然后逐级向上传播到较为不具体的节点或文档。
  • 事件捕获:事件开始由不太具体的节点接收,然后逐级向下传播到最具体的节点。它与事件冒泡是相反的过程

事件委托 - 图3

click 事件发生的顺序 :

  1. <ul id="myLink">
  2. <li id="1">aaa</li>
  3. <li id="2">bbb</li>
  4. <li id="3">ccc</li>
  5. </ul>

事件冒泡 li -> ul -> body -> html -> document ->window

事件捕获 window -> document-> html -> body -> ul ->li

事件委托概念

通俗的说就是将元素的事件委托给它的父级或者更外级的元素处理,它的实现机制就是事件冒泡

不使用事件委托

  1. var myLink = document.getElementById("myLink");
  2. var li = myLink.getElementsByTagName("li");
  3. for (var i = 0; i < li.length; i++) {
  4. li[i].onclick = function (e) {
  5. var e = event || window.event;
  6. var target = e.target || e.srcElement;
  7. alert(e.target.id + ":" + e.target.innerText);
  8. };
  9. }
  • 给每一个列表都绑定事件,消耗内存
  • 当有动态添加的元素时,需要重新给元素绑定事件
  1. function delegate(element, eventType, selector, fn) {
  2. element.addEventListener(eventType, (e) => {
  3. let el = e.target;
  4. while (!el.matches(selector)) {
  5. if (element === el) {
  6. el = null;
  7. break;
  8. }
  9. el = el.parentNode;
  10. }
  11. el && fn.call(el, e, el);
  12. });
  13. return element;
  14. }

事件委托的优点

  • 只需要将同类元素的事件委托给父级或者更外级的元素,不需要给所有的元素都绑定事件减少内存占用空间,提升性能。
  • 动态新增的元素无需重新绑定事件

事件委托的实现依靠的冒泡,因此不支持事件冒泡的事件就不适合使用事件委托。

不是所有的事件绑定都适合使用事件委托,不恰当使用反而可能导致不需要绑定事件的元素也被绑定上了事件。

addEventListener 监听器方法第三个参数

  1. element.addEventListener(event, function, useCapture)

useCapture
可选。布尔值,指定事件是否在捕获或冒泡阶段执行。

可能值:

  • true - 事件句柄在捕获阶段执行(即在事件捕获阶段调用处理函数)
  • false- 默认。事件句柄在冒泡阶段执行(即表示在事件冒泡的阶段调用事件处理函数)

阻止事件冒泡

  1. 给子级加 event.stopPropagation( )
  1. $("#div1").mousedown(function (e) {
  2. var e = event || window.event;
  3. event.stopPropagation();
  4. });
  1. 在事件处理函数中返回 false
  1. $("#div1").mousedown(function (event) {
  2. var e = e || window.event;
  3. return false;
  4. });

但是这两种方式是有区别的。return false 不仅阻止了事件往上冒泡,而且阻止了事件本身(默认事件)。event.stopPropagation()则只阻止事件往上冒泡,不阻止事件本身。

  1. event.target==event.currentTarget,让触发事件的元素等于绑定事件的元素,也可以阻止事件冒泡;

event.currentTarget 告诉我们该事件附加在哪个元素上,或者其 eventListener 触发了该事件的元素。
event.target 告诉事件从哪里开始。

给父节点绑定事件做冒泡的事件代理的原因是:evt.target 永远拿到的都是最深层的目标节点,在点击节点的时候,由于冒泡的原因,触发了父节点的点击事件,这个时候可以通过 evt.target 拿到真正点击的深层子节点,然后做进一步逻辑

注意点

  1. 事件流顺序
  2. 事件委托原理->事件冒泡的应用
  3. addEventListener 第三个参数 默认是 false 事件冒泡时触发 true 为事件捕获阶段触发
  4. e.target 和 e.currentTarget 的区别