概念
要聊事件委托,那么就要先了解JS的事件流。
事件流
在JS中,事件流都会经历三个阶段:
事件捕获阶段
从window对象传导到目标节点。也就是由外层到内层,即从DOM树的父到子。该阶段中,事件的传递流程:
首先事件会从document一直向下传递,直到传递到目标元素,事件在传递的过程中,还会依次检查,传递路径上所经过的节点,是否绑定了该事件的监听函数,如果有则执行
处于目标阶段
该阶段顾名思义,就是事件传递到对应的目标元素,并且触发了目标元素的监听函数。
该阶段也是事件冒泡的第一个阶段,也就是冒泡阶段的target触发事件,所以目标阶段也会被当做事件冒泡的一部分
事件冒泡阶段
冒泡阶段。学过冒泡排序的都是知道,冒泡指的是一层一层的进行冒泡。所以事件冒泡,就是从目标节点一层一层向上传递,直到传回window对象。即由内到外。
并且在传递的过程中,也会依次检查经过的节点是否绑定了事件监听函数,如果有则执行。
下图是W3C官网的事件流模型图。
红色的是事件捕获阶段,蓝色的就是处于目标阶段,而绿色的是事件冒泡阶段
由上图能够看出,事件的阶段传递顺序。【事件捕获】—>【目标阶段】—>【事件冒泡】
事件流与事件委托的关系
我们知道了,在事件的执行过程中,事件是会进行传递的。所以我们可以理解为,就算不把事件绑定在对应的节点上,也是能够触发该节点的对应事件的。
换句话来说,父元素绑定了点击事件,但是子元素不用绑定,当点击子元素时,同样会触发点击事件。
但这时,有个问题就出现了,在事件捕获,或者是事件冒泡的阶段,在这两个阶段中,都会经过目标元素,进行触发。那么我们怎么控制触发的时机呢?其实,在我们平常绑定监听事件时,默认的触发时机是在冒泡阶段进行触发
这是我们平常绑定事件的基础方式
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事件统一冒泡到根节点上,然后统一做处理。