深入理解-事件委托

深入理解-事件委托

2016-11-20 15:02javascriptweb开发综合性能优化JS性能优化事件冒泡事件委托事件委托优化事件委托导致性能损失 65 views

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

概述

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

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

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

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

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

时间: 02-04

深入理解-事件委托的相关文章

理解事件委托(1):js中的事件委托

什么是事件委托:通俗的讲,事件就是onclick,onmouseover,onmouseout,委托呢,就是让别人来做,这个事件本来是加在某些元素上的,然而你却加到别人身上来做,完成这个事件. 也就是:利用冒泡的原理,把事件加到父级上,触发执行效果. 好处呢:1,提高性能. 我们可以看一个例子:需要触发每个li来改变他们的背景颜色. <body> <ul id="number"> <li>a</li> <li>b</li

JavaScript事件委托的技术原理

如今的JavaScript技术界里最火热的一项技术应该是‘事件委托(event delegation)’了.使用事件委托技术能让你避免对特定的每个节点添加事件监听器:相反,事件监听器是被添加到它们的父元素上.事件监听器会分析从子元素冒泡上来的事件,找到是哪个子元素的事件.基本概念非常简单,但仍有很多人不理解事件委托的工作原理.这里我将要解释事件委托是如何工作的,并提供几个纯JavaScript的基本事件委托的例子. 假定我们有一个UL元素,它有几个子元素: <ul id="parent-l

js的事件委托机制

如今的JavaScript技术界里最火热的一项技术应该是'事件委托(event delegation)'了.使用事件委托技术能让你避免对特定的每个节点添加事件监听器:相反,事件监听器是被添加到它们的父元素上.事件监听器会分析从子元素冒泡上来的事件,找到是哪个子元素的事件.基本概念非常简单,但仍有很多人不理解事件委托的工作原理.这里我将要解释事件委托是如何工作的,并提供几个纯JavaScript的基本事件委托的例子. 假定我们有一个UL元素,它有几个子元素: <ul id="parent-l

JavaScript 事件委托的技术原理

如今的 JavaScript 技术界里最火热的一项技术应该是'事件委托(event delegation)'了.使用事件委托技术能让你避免对特定的每个节点添加事件监听器:相反,事件监听器是被添加到它们的父元素上.事件监听器会分析从子元素冒泡上来的事件,找到是哪个子元素的事件.基本概念非常简单,但仍有很多人不理解事件委托的工作原理.这里我将要解释事件委托是如何工作的,并提供几个纯JavaScript的基本事件委托的例子. 假定我们有一个UL元素,它有几个子元素: 1 2 3 4 5 6 7 8 <

前端面试必考:事件委托

先看几道面试题 描述下js里面的事件流 默认情况下,事件是在冒泡阶段执行还是捕获阶段执行 请简要说明事件委托原理和使用场景 手写原生js实现事件代理,注意浏览器兼容 如果上面的面试题,您不能很顺利的作答,那么希望这篇文件对您能有一些帮助.如果出现错误,请您及时指正,谢谢. 什么是事件委托 事件委托也叫事件代理,<Javascript高级程序设计>中写道:事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件. 想要理解事件委托,需要先理解js事件流. js事件流 图为事

通俗粗暴的事件委托理解

简单粗暴的事件委托理解代码 1 using System; 2 using System.Collections.Generic; 3 4 public class MyClass 5 { 6 public static void RunSnippet() 7 { 8 Header head=new Header(); 9 XiaShuA xiashuaA=new XiaShuA(head); 10 XiaShuB xiashuaB=new XiaShuB(head); 11 head.Rais

asp.net事件委托易理解实例

比如说一个公司(场景),你是老板,手下有两个员工,小张和小王. 你命令小王,如果小张玩游戏,则小王扣去小张500元钱.这就是现实中的委托.实际上,在写程序中,程序员就是老板,小张和小王就是两个对象.小张玩游戏是一个方法,小张还有一个游戏事件,他玩游戏激发这个事件.而小王就是事件处理对象,他负责把小张的钱扣除500.所以,委托有如下几个要素:1 激发事件的对象--就是小张2 处理对象事件的对象--就是小王3 定义委托,就是你让小王监视小张.如果这三个要素都满足的话,则你就写出了一个完整事件的处理.

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

最近在深入实践js中,遇到了一些问题,比如我需要为动态创建的DOM元素绑定事件,那么普通的事件绑定就不行了,于是通过上网查资料了解到事件委托,因此想总结一下js中的事件绑定与事件委托. 事件绑定   最直接的事件绑定:HTML事件处理程序 如下示例代码,通过节点属性显式声明,直接在HTML中,显式地为按钮绑定了click事件,当该按钮有用户点击行为时,便会触发myClickFunc方法. /* html */ <button id="btn" onclick="myCl

理解js事件冒泡事件委托事件捕获

js事件冒泡 javascript的事件传播过程中,当事件在一个元素上出发之后,事件会逐级传播给先辈元素,直到document为止,有的浏览器可能到window为止,这就是事件冒泡现象. <div id="col">    <p>        <a id="btn" href="#">button</a>    </p></div> <script> let b