很多人是在使用事件委托的,那对于一个使用者来说,只要能正确的使用好事件委托,完成工作,就算可以了,那么你有认真的考虑过事件委托的原理,以及你的使用场景是否适合使用事件委托呢,如果需要使用事件委托,那么你是否有正确的使用呢?这里我想简单的说一下我对事件委托的理解,希望可以有机会多多交流。
概述
事件委托有哪些好处,才会被现在人们大量的使用呢?
那么就得先说说事件的一些性能和使用的问题:
1:绑定事件越多,浏览器内存占用越大,严重影响性能。
2:ajax
的出现,局部刷新的盛行,导致每次加载完,都要重新绑定事件
3:部分浏览器移除元素时,绑定的事件并没有被及时移除,导致的内存泄漏,严重影响性能
4:大部分ajax
局部刷新的,只是显示的数据,而操作却是大部分相同的,重复绑定,会导致代码的耦合性过大,严重影响后期的维护。
这些个限制,都是直接给元素事件绑定带来的问题,所以经过了一些前辈的总结试验,也就有了事件委托这个解决方案。
我们本篇将要说的是,事件委托。
事件委托的基础
如果我们相对一个技术点了解的更深,用的更好,那么我们就需要对这个技术点的原理有更多的了解,那么事件委托的实现原理是什么呢?
1:事件的冒泡,所以才可以在父元素来监听子元素触发的事件。
2:DOM
的遍历,一个父级元素包含的子元素过多,那么当一个事件被触发时,是否触发了某一种类型的元素呢?
这是事件委托的两个基础,也是事件委托的核心,跟事件委托相关的技术点,如果碰到什么问题,都可以在这两个点进行切入,来寻求解决方案。
而且还有一点要注意:不管你使用什么样的框架,实现方案,这两个基础都是必须的,OK
,我们继续看下去。
一个简单的事件委托
只是使用文字描述,是无法很好的理解事件委托的,那么这里我们来看一个例子:
注:假设只支持标准浏览器,不兼容IE的低版本
我现在使用原生的JS
,来实现一个简单的事件委托
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
function _addEvent(obj,type,fn){
obj.addEventListener(type,fn,false);
}
function _delegate(obj,className,fn){
var dc = " "+className+ " ";
function cb(e){
var target = e.target,
c = "";
while(target != obj){
c = " "+target.getAttribute("class")+" ";
if(c.indexOf(dc) != -1){
fn.call(target,e);
}
target = target.parentNode;
}
}
_addEvent(obj,"click",cb);
}
然后,可以直接这么调用:_delegate(document.body,"item",fn);
它执行的效果是:body
内部,所有class
包含item
的元素,都会相应该操作。
查看示例:DEMO
注:该方法是为了说明这个原理,并不是用于生产开发中的,如果想要用在生产开发中,那么实现方式应该更严谨,一些必要的类型检测,还是需要的。
jQuery中的事件委托的实现
我前面说的,不管使用什么样的技术方案,都不能抛开事件委托的两个基础,那么我们就看看jQuery
库的实现方法吧(其他的库,都没有去看,汗~~);
暂且不论事件绑定,各个地方是如何处理的,当事件冒泡到绑定的元素上时,要做出相应的时候,会有下面的一段函数:
jQuery.event.handlers
函数,用来查看所有包含事件委托,和直接绑定的回调函数的,源代码如下:(源代码来自jQuery v3.1.1
版本)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
//前面是一些判断,判断如果该元素之前有被绑定过事件委托,
//并且符合一些其他的限制(比如:点击不是右键,元素不是txt元素等)的时候,
//就会执行到这里:
//cur = event.target
//
//cur,直接从target自this的DOM遍历
for ( ; cur !== this; cur = cur.parentNode || this ) {
// Don't check non-elements (#13208)
// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
// 判断是否为对应的元素,Element元素,
// type = "click"的元素,不能被disabled。
if ( cur.nodeType === 1 &&
!( event.type === "click" &&
cur.disabled === true ) ) {
matchedHandlers = [];
matchedSelectors = {};
//delegateCount表示,该元素,被绑定了多少次事件委托,
//把这些个委托事件,都遍历一遍
for ( i = 0; i < delegateCount; i++ ) {
handleObj = handlers[ i ];
// Don't conflict with Object.prototype properties (#13203)
sel = handleObj.selector + " ";
if ( matchedSelectors[ sel ] === undefined ) {
matchedSelectors[ sel ] = handleObj.needsContext ?
jQuery( sel, this ).index( cur ) > -1 :
jQuery.find( sel, this, null, [ cur ] ).length;
}
//如果符合,则把回调函数推入一个数组中。
if ( matchedSelectors[ sel ] ) {
matchedHandlers.push( handleObj );
}
}
//如果当前的cur元素,找到了需要回调的函数,那么就把相关的数据,
//推入到handlerQueue数组中,在最后handlerQueue会被返回
//在另外的dispatch函数中,按顺序执行,来触发这些回调
if ( matchedHandlers.length ) {
handlerQueue.push( { elem: cur, handlers: matchedHandlers } );
}
}
}
从上面的代码中,来验证,jQuery
中,事件委托的原理同样离不开DOM
的查找。
那么同样,你是否有注意到,在事件委托中,到底执行了多少次的DOM
查找呢?
1:从目标event.target
到绑定事件的元素ele
之间,有多少层的DOM
结构,假设为x
,也就是前面源代码中的cur
的for
语句遍历;
2:在ele
元素上,绑定过多少次的事件委托,假设为y
,也就是前面源代码中的delegateCount
数据。
那么在每次触发ele
区域的type
事件之后,就需要遍历的DOM
结构的次数是x*y
;也就是源代码中,两个for
语句执行的次数。
如果按照这个计算来看,那么层级越多,事件委托的绑定次数越多,那么在每次触发type事件时,需要查找DOM的次数就越多。
事件委托的缺点
说到这里,还有一个问题就是,我们应该都知道,JS
的运行速度还是很不错的,尤其是一些现代浏览器,而浏览器中的DOM
操作,却是非常耗费性能的,那么在事件委托的时候,这些DOM
操作,是否会影响整个页面的运行性能呢?
这无疑是肯定的,前面,我们根据jQuery
的源码看到了,DOM
遍历的次数与DOM
结构的层数,和事件委托绑定的个数有关。
这个说法对于click
这样的事件来说,消耗还算少的话;
那么对于随时都会触发的mouseover
等事件来说,这个消耗,是否看起来就比较可观了呢?
如果再考虑到一些性能不好设备,使用了性能不好的浏览器呢?这个消耗又会是怎么样的呢?
综合上述的考虑,你是否愿意,认真的考虑一下,在使用事件委托的时候,是否符合你的使用场景呢,是否真的有必要,随意的去使用事件委托呢?
先看两个例子吧:
同样使用jQuery
的事件委托,同样是100个元素:
1:使用一次事件委托,委托到所有的元素- DEMO
2:使用100个事件委托,每个都委托一个元素 – DEMO
这个是一个简单的例子,也属于比较极端的例子,只是为了验证这个东西,我使用timeline
测试一次点击事件,耗费的时间比,得到的结果如下图所示:
使用一次事件委托,委托到所有的元素
使用100个事件委托,每个都委托一个元素
这还是在没有其他事件的情况下:
接下来我们看看,如果我们监听的是mouseover
这个事件呢?
测试DEMO的链接:
1:使用一次事件委托,委托到所有的元素- DEMO
2:使用100个事件委托,每个都委托一个元素 – DEMO
得到的数据:
使用一次事件委托,委托到所有的元素:
使用100个事件委托,每个都委托一个元素
如果是这样的话,那这个消耗是否看起来更可观了,这里的情况还比较单一,如果再一个很复杂的页面,交叉着使用这些呢?
什么时候选择使用事件委托
完美是不存在的,任何的东西都有它的两面性,都是有好有坏,选择一个就要在拥有它的好的同时,接受它的坏的地方,就像是男女之间,如果都想找那个完美的另一半,那么还是选择孤独终老吧(这个应该更简单),所以这个时候,只要我们能看到好的同时,也可以接受那一些不好的,退一步海阔天空嘛~~~
所以,事件委托也是这样的,如果事件委托没有缺点,那么它就不仅仅是一个解决方案了,而是会被浏览器直接纳入规范了吧,那么当前的事件绑定规范,就要直接改掉了
既然如此,那么什么时候,才适合使用事件委托呢,如何能更优的使用呢?
结合前面我们说到的,事件委托影响性能的因素:
1:元素中,绑定事件委托的次数;
2:点击的最底层元素,到绑定事件元素之间的DOM
层数;
结合这三点,在必须使用事件委托的地方,可以如下的处理:
1:只在必须的地方,使用事件委托,比如:ajax
的局部刷新区域
2:尽量的减少绑定的层级,不在body
元素上,进行绑定
3:减少绑定的次数,如果可以,那么把多个事件的绑定,合并到一次事件委托中去,由这个事件委托的回调,来进行分发。
说到这里,也只能算是有了一个最基础的结论,但是呢?总的有个解决方案吧,不然…
提高事件委托性能的解决方案
看完前面的事件委托的一些瓶颈之外,现在要给出一些解决的方案了:
1:降低层级,这个比较好实现,在开发中,直接把事件绑定在低层级的元素上即可,这个无法继续优化;
2:减少绑定的次数,现在只能在这个点上继续优化了。
所以,在这里,来看看我的解决方案(基于jQuery
/Zepto
的),在我的解决方案中,我固化了一些东西,比如,使用事件委托时,不在使用class
等一些常用的选择器,而是使用”data-
“类型的属性选择器,我先在这里说使用的方法,后面再看示例:
假设我准备要绑定事件的元素是wrapper
(jQuery
实例化的)元素,我准备给它绑定一系列的click
事件,那么就需要如下的使用方法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
var wrapper = $("#wrapper")
wrapperClick = eventMediator(wrapper,"click");
//给wrapper初始化一个click的事件委托
//那么表示,在wrapper元素子元素中,只要元素中有data-click的元素,会被覆盖
//添加一个click1的回调,那么表示如果点击的目标元素中,包含data-click="click1"的元素
//可以执行该回调
wrapperClick.add("click1",function(){});
wrapperClick.add("click2",function(){});
wrapperClick.add("click3",function(){});
//这个,我们在wrapper元素上,绑定的事件委托,其实就是有三种回调,那么
//当元素当元素的而具体执行哪一个回调,就与子元素的data-click的属性值有关
//data-click = "click1"的元素,执行绑定的第一个回调
//data-click = "click2"的元素,执行绑定的第二个回调
//data-click = "click3"的元素,执行绑定的第三个回调
如此,则可以实现,在一个元素上,绑定一次事件委托,可以根据data-click
的不同,执行不同的回调。
其中eventMediator方法中,返回的对象,除了包含有add方法(注册一个回调)之外,还包含一个移除的方法,remove方法,通过remove方法(使用方法与add一样,传的参数也一样),可以直接移除之前的一个注册(匿名函数不能被移除)。
使用这样的方法,就可以做到虽然我这个区域,不同的元素需要不同的回调函数,而我也只需要一个事件委托,就可以解决这个问题,那么事件委托中,每次触发事件导致的DOM
查找,就只受限于DOM
的层数了,这也就可以有效的降低了因为DOM查找带来的损耗了,接下来我们看看一些对比:
1:click
事件,100次不同的回调
直接使用jQuery
的事件委托:DEMO
优化后事件委托的DEMO :DEMO
直接使用jQuery
事件委托:
优化后的事件委托:
2:mouseover事件,100次不同的回调
直接使用jQuery的DEMO:DEMO
优化后的DEMO :DEMO
直接使用jQuery
事件委托:
优化后的事件委托:
至于具体的使用方法,请查看DEMO
哦,以及源代码的实现方式,都可以在DEMO
找到的。
并且,您可以试试,回调函数,和直接使用jQuery
绑定时的回调函数,有什么区别,说不定你会爱上这个方案呢,哈~~
结尾
我这里的DEMO虽然把绑定回调的函数设置为100个,虽然一个项目中,事件委托的个数不会有这么多,但是一个真正的项目,所处的环境,毕竟会比这里的DEMO复杂好多,所以这里就把这个设置为100,相信与真正项目中的环境,更接近一些吧。
说到这里,算是结束了,如过您发下文中有描述错误或者不当的地方,请帮忙指正,谢谢!