• 深入理解事件委托


    链接

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

    概述

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

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

    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,相信与真正项目中的环境,更接近一些吧。

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

  • 相关阅读:
    asp.net core MVC 入门学习
    C# 中关于重载与重写的区别及用法
    C# 生成随机的6位字母,包含大小写
    C# CheckBoxList绑定值,设置及获取
    jquery中checkbox的全选与反选
    docker 执行 docker system prune 导致Azure Devops build镜像失败
    Navicat 连接mysql 报错: Authentication plugin caching_ sha2_password cannot be loaded
    docker build 错误 /usr/share/dotnet/sdk/2.1.801/Microsoft.Common.CurrentVersion.targets(2106,5): warning MSB3245: Could not resolve this reference
    Azure Devops: COPY failed: stat /var/lib/docker/tmp/docker-builder268095359/xxxxxxx.csproj no such file or directory
    Azure DevOps vsts-agent-linux 安装出错, Must not run with sudo
  • 原文地址:https://www.cnblogs.com/dhsz/p/8883645.html
Copyright © 2020-2023  润新知