深入理解-事件委托

Posted 最骚的就是你

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解-事件委托相关的知识,希望对你有一定的参考价值。

深入理解-事件委托

很多人是在使用事件委托的,那对于一个使用者来说,只要能正确的使用好事件委托,完成工作,就算可以了,那么你有认真的考虑过事件委托的原理,以及你的使用场景是否适合使用事件委托呢,如果需要使用事件委托,那么你是否有正确的使用呢?这里我想简单的说一下我对事件委托的理解,希望可以有机会多多交流。

概述

事件委托有哪些好处,才会被现在人们大量的使用呢?

那么就得先说说事件的一些性能和使用的问题:

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,也就是前面源代码中的curfor语句遍历;

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-“类型的属性选择器,我先在这里说使用的方法,后面再看示例:

假设我准备要绑定事件的元素是wrapperjQuery实例化的)元素,我准备给它绑定一系列的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,相信与真正项目中的环境,更接近一些吧。

说到这里,算是结束了,如过您发下文中有描述错误或者不当的地方,请帮忙指正,谢谢!

本文属于原创文章,转载请注明出处,谢谢!




以上是关于深入理解-事件委托的主要内容,如果未能解决你的问题,请参考以下文章

c# 深入理解事件2.0

理解Javascript中的事件绑定与事件委托

C#编程之委托与事件四

C#委托,事件最初浅的和最易看懂的学习笔记

委托与事件

C# 再次理解委托事件与函数作为参数