聊聊事件委托

70 阅读8分钟

概念

要聊事件委托,那么就要先了解JS的事件流。

事件流

在JS中,事件流都会经历三个阶段:

事件捕获阶段

从window对象传导到目标节点。也就是由外层到内层,即从DOM树的父到子。该阶段中,事件的传递流程:

首先事件会从document一直向下传递,直到传递到目标元素,事件在传递的过程中,还会依次检查,传递路径上所经过的节点,是否绑定了该事件的监听函数,如果有则执行

处于目标阶段

该阶段顾名思义,就是事件传递到对应的目标元素,并且触发了目标元素的监听函数。

该阶段也是事件冒泡的第一个阶段,也就是冒泡阶段的target触发事件,所以目标阶段也会被当做事件冒泡的一部分

事件冒泡阶段

冒泡阶段。学过冒泡排序的都是知道,冒泡指的是一层一层的进行冒泡。所以事件冒泡,就是从目标节点一层一层向上传递,直到传回window对象。即由内到外。

并且在传递的过程中,也会依次检查经过的节点是否绑定了事件监听函数,如果有则执行。

下图是W3C官网的事件流模型图。

红色的是事件捕获阶段,蓝色的就是处于目标阶段,而绿色的是事件冒泡阶段 eventflow.svg

由上图能够看出,事件的阶段传递顺序。【事件捕获】—>【目标阶段】—>【事件冒泡】

事件流与事件委托的关系

我们知道了,在事件的执行过程中,事件是会进行传递的。所以我们可以理解为,就算不把事件绑定在对应的节点上,也是能够触发该节点的对应事件的。

换句话来说,父元素绑定了点击事件,但是子元素不用绑定,当点击子元素时,同样会触发点击事件。

但这时,有个问题就出现了,在事件捕获,或者是事件冒泡的阶段,在这两个阶段中,都会经过目标元素,进行触发。那么我们怎么控制触发的时机呢?其实,在我们平常绑定监听事件时,默认的触发时机是在冒泡阶段进行触发

这是我们平常绑定事件的基础方式

var btn = document.getElementById(".btn");
btn.addEventListener("click", handle);

但其实,addEventListener是接收三个参数的

addEventListener(eventType, handler, useCapture);
【事件类型,事件处理函数,事件是否在捕获阶段进行处理】

我们使用平常方式绑定时,默认设置为false,是为了与【IE浏览器保持一致】,这里牵扯出另一个点。

事件模型

事件模型可以分为三种。

  • 1.原始事件模型【DOM0级】
    • 就是直接在节点上使用原生属性进行绑定的方式
    • // 行内绑定
      <input type="button" onclick={fn()} />;
      
      // 通过JS进行手动绑定
      var btn = document.getElementById('.btn');
      btn.onclick = fn;
      
    • 该绑定方式:
      • 绑定方式速度快
      • 并且兼容性强,会以最快的熟读绑定。但在这个过程中,由于绑定速度过快,当页面并未渲染完成时,事件是不能够正常运行的。
      • 该方式,只支持冒泡,不支持捕获。
      • 同一个类型的事件,只能绑定一次,后绑定的会覆盖之前绑定的
  • 2.标准事件模型【DOM2级】
    • 前面说的,使用【addEventListener】绑定事件的方式,是事件模型中的标准事件模型。
    • 并且事件的三个执行流程,也是针对的标准事件模型。
    • 特性
      • 可以在同个节点的同个事件绑定多个处理函数,并且不会冲突。
      • 可以控制事件函数的执行时机
      • addEventListener(eventType, handler, useCapture); 
        【事件类型,事件处理函数,事件是否在捕获阶段进行处理】
        // 我们使用平常方式绑定时,默认设置为false
        // 是为了与【IE浏览器保持一致】
        
  • 3.IE事件模型【基本不用】
    • 该事件模型也就是IE浏览器的事件模型。
    • 该事件模型只有两个过程
      • 事件处理阶段:也就是事件到达目标节点,触发事件处理函数。
      • 事件冒泡阶段。
      • 绑定方式
        • var btn = document.getElementById('.btn');
          btn.attachEvent(‘onclick’, showMessage);
          

关系

我们知道了事件流模型。那么我们就可以利用事件流来给元素处理事件。

委托:将xxx【委托】给xxx

在实际应用场景中,我们会把一个或多个元素的事件【委托】给它的父元素,或者是更外层的元素身上。也就是真正监听事件的元素的是外层元素,而不是对应的目标元素。

所以要实现【委托】的这个操作,我们就要用到JS的事件冒泡机制。

当该事件已经到达目标元素上时,发现该目标元素并没有监听该事件的处理函数,但是由于JS有事件冒泡机制,就会向上冒泡,从而触发外层元素的监听该事件的处理函数

那这里又会出现一个新的问题,那如果将监听事件的处理函数绑定在外层元素的话,那在JS的事件捕获阶段不就已经触发了该事件吗?

  • 这里就涉及到事件模型了,因为在我们监听事件并绑定处理函数时,所使用的事件模型是标准事件模型
addEventListener(eventType, handler, useCapture); 
【事件类型,事件处理函数,事件是否在捕获阶段进行处理】
// 我们使用平常方式绑定时,默认设置为false
// 是为了与【IE浏览器保持一致】
  • 所以该处理函数不会触发在事件捕获阶段。

为什么要用事件委托来监听事件

总的两点来说

  • 就是为了减少整个页面在运行时的内存,提升性能
  • 减少不必要的重复绑定工作

提升性能:节省内存的占用,减少事件注册

  • 我们都知道,为元素绑定监听事件时,会在内存中开辟一块空间
  • 一个监听事件就是一个对象,每多监听一个事件函数,就会多占用一块内存。
  • 如果我们使用事件委托,只对它的父级或者外层元素进行监听,这样我们只绑定一个监听函数。
  • 操作DOM的次数也只需要一次。这样我们就能够减少与DOM的交互次数,减少内存占用,提高页面性能。
  • 设置事件处理程序所需时间更少, 加快了整个页面的交互就绪时间
    • 在访问 DOM 方面, 也使得 DOM 访问次数减少。试想一下, 如果要为许多的 DOM 元素绑定事件, 自然需要多次访问 DOM 元素, 设置事件处理程序所需时间更长, 整个页面就绪需要的时间越多
  • 并且,我们移除监听函数时,也只需要操作一次。

例子

<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  <li>item n</li>
</ul>

比如,当我们想要为上面的li进行事件监听时,就可以利用事件委托,这样我们就不用为每个li绑定监听事件处理函数,只需要给ul绑定就好了。

  • 那怎么知道点击的是哪个li呢,这时可以利用事件对象【event】
// 给父层元素绑定事件
document.getElementById("list").addEventListener("click", function (e) {
  // 兼容性处理
  var event = e || window.event;
  var target = event.target || event.srcElement;
  // 判断是否匹配目标元素
  // 或者可以给元素绑定某个属性进行匹配
  if (target.nodeName.toLocaleLowerCase === "li") {
    console.log("the content is: ", target.innerHTML);
  }
});

为动态添加出来元素绑定监听事件

如果这个列表是会动态添加和移除li的,那我们如果绑定在目标元素上,那就会频繁操作DOM,进行绑定和移除操作。会影响页面性能。

但是我们如果用了事件委托,只需要绑定在外层元素上,无论如何添加或移除,我们只需要操作DOM的添加和移除,不用额外操作DOM事件的监听和移除。

因为在li上所触发的点击事件,都会冒泡到ul上,这样即不会影响事件监听的准确度,也可以减少操作DOM的次数。

局限性

不是所有解决方法都是完美的。只是看是否针对需求来说这是最佳的解决方案。

【一】:没有冒泡性质的事件不能使用事件委托。

  • 毕竟事件委托的原理就是事件冒泡。无法触发冒泡机制,自然也不能使用事件委托。
  • 下面的事件不支持冒泡
    • scroll、focus、blur、resize
    • mouseleave【至于鼠标移出事件为什么会不会触发事件冒泡】,我觉得可以研究研究【mouseleave 事件】
    • Media:由视频、图像、音频等媒体触发的相关事件,都不会触发冒泡

【二】:层级过多,事件在冒泡的过程中,可能会在某层被阻止了或者被触发了(误判)。

  • 理论上委托会建议就近委托,比如在table上代理td,而不是在document上代理td。
    • 因为如果使用远程外层元素进行代理,可能在中间的某一层,禁止事件冒泡操作,那么就会导致事件丢失,不会进行触发。
  • 如果在document中代理了所有button的click事件,另外的人在引用改js时,可能不知道,造成单击button触发了两个click事件
  • react中的onClick就是用的事件委托,将onClick事件统一冒泡到根节点上,然后统一做处理。