关于编写性能高效的javascript事件的技术

如何能作出高效的web前端程序是我每次作前端开发都会不自觉去考虑的问题。几年前雅虎里牛逼的前端工程师们出了一本关于提高web前端性能的书籍,轰动了整个web开发技术界,让神秘的web前端优化问题成为了大街的白菜,web前端优化变成了菜鸟和大牛都能回答的简单问题,当整个业界都知道了惊天秘密的答案,那么现有的优化技术已经不能对你开发的网站产生的质的飞越,为了让咱们开发的网站性能比别人的网站更加优秀,咱们须要更加深刻的独立思考,储备更加优秀的技能。javascript

  Javascript里的事件系统是我想到的第一个突破点。为何会是javascript的事件系统呢?咱们都知道web前端包含三个技术:html、css和javascript,html和css如何结合真是一目了然:style、class、id以及html标签,这个没啥好讲的,可是javascript是如何切入到html和css中间,让三者融合呢?最后我发现这个切入点就是javascript的事件系统,无论咱们写多长多复杂的javascript代码,最终都是经过事件系统体如今html和css上,所以我就在想既然事件系统是三者融合的切入点,那么一个页面里,特别是当今愈来愈复杂的网页里必然会有大量事件操做,没有这些事件咱们精心编写的javascript代码只有刀枪入库,英雄无用武之地了。既然页面会存在大量事件函数,那么咱们按习惯写事件函数,会存在影响效率的问题吗?我研究下来的答案是真有效率问题,并且仍是严重的效率问题。css

  为了说清楚个人答案,我要先详细讲解下javascript的事件系统。html

  事件系统是javascript和html以及css融合的切入点,这个切人点比如java里的main函数,一切神奇都是由这里开始,那么浏览器是如何完成这种切入呢?我研究下来一共有3种方式,它们分别是:前端

  方式一:html事件处理java

  html事件处理就是将事件函数直接写在html标签里,由于这种写法和html标签紧耦合,因此称为html事件处理。例以下面代码:程序员

<input type="button" id="btn" name="btn" onclick="alert('Click Me!')"/>

  

  若是click事件函数复杂了,这么写代码确定会带来不便,所以咱们经常把函数写在外部,onclick直接调用函数名,例如:web

复制代码
<input type="button" id="btn" name="btn" onclick="btnClk()"/>

function btnClk(){

         alert("click me!");   

}
复制代码

  

  上面这个写法是一种很美的写法,因此时下仍是不少人会不自觉的使用它,可是也许不少人不知道,后一种写法其实没有前一种写法健壮,这个也是我前不久在研究非阻塞加载脚本技术时候碰到的问题,由于根据前端优化的原则,javascript代码每每是位于页面的底部,当页面有被脚本阻塞时候,html标签里引用的函数可能还没执行到,这个时候咱们点击页面按钮,结果会报出“XXX函数未定义的错误”,在javascript里这样的错误是会被try,catch所捕获,所以为了让代码更加健壮,咱们会有以下的改写:ajax

<input type="button" id="btn" name="btn" onclick="try{btnClk();}catch(e){}"/>

  

  看到上面代码岂是一个恶心能描述的。浏览器

       方式二:DOM0级事件处理前端工程师

       DOM0级事件处理是当今全部浏览器都支持的事件处理,不存在任何兼容性问题,看到这样一句话都会让每一个作web前端的人们激动不已。DOM0事件处理的规则是:每一个DOM元素都有本身的事件处理属性,该属性能够赋值一个函数,例以下面的代码:

复制代码
var btnDOM = document.getElementById("btn");

btnDOM.onclick = function(){

         alert("click me!");            

}
复制代码

  

  DOM0级事件处理的事件属性都是采用“on+事件名称”的方式定义,整个属性都是小写字母。咱们知道DOM元素在javascript代码里就是一个javascript对象,所以从javascript对象角度理解DOM0级事件处理就很是容易,例以下面代码:

btnDOM.onclick = null;

  

  那么按钮的点击事件被取消了。

  再看下面的代码:

复制代码
btnDOM.onclick = function(){

         alert("click me!");            

}

 

btnDOM.onclick = function(){

         alert("click me1111!");            

}
复制代码

  

  后面一个函数会将第一个函数覆盖。

  方式三:DOM2事件处理和IE事件处理

  DOM2事件处理是标准化的事件处理方案,可是IE浏览器本身搞了一套,功能和DOM2事件处理类似,可是代码写起来就不太同样了。

  在讲解方式三以前,我必需要补充一些概念,不然是没法讲清楚方式三的内涵。

  第一个概念是:事件流

       在页面开发里咱们经常会碰到这样的状况,一个页面的工做区间在javascript能够用document表示,页面里有个div,div等因而覆盖在document元素上,div里面有个button元素,button元素是覆盖在div上,也等于覆盖着document上,因此问题来了,当咱们点击这个按钮时候,这个点击行为其实不只仅发生在button之上,div和document都被做用了点击操做,按逻辑这三个元素都是能够促发点击事件的,而事件流正是描述上述场景的概念,事件流的意思是:从页面接收事件的顺序。

      第二个概念:事件冒泡和事件捕获

      事件冒泡是微软公司提出解决事件流问题的方案,而事件捕获则是网景公司提出的事件流解决方案,它们的原理以下图:

 

       冒泡事件由div开始,其次是body,最后是document,事件捕获则是倒过来的先是document,其次是body,最后是目标元素div,相比之下,微软公司的方案更加人性化符合人们的操做习惯,网景的方案就很别扭了,这是浏览器大战的恶果,网景慢了一步就以牺牲用户习惯的代码解决事件流的问题。

       微软公司结合冒泡事件设计了一套新的事件系统,业界习惯称为ie事件处理,ie事件处理方式以下面代码所示:

复制代码
var btnDOM = document.getElementById("btn");

btnDOM.attachEvent("onclick",function(){

         alert("Click Me!");

});
复制代码

 

       在ie下经过DOM元素的attachEvent方法添加事件,和DOM0事件处理相比,添加事件的方式由属性变成了方法,因此咱们添加事件就须要往方法里传递参数,attachEvent方法接收两个参数,第一个参数是事件类型,事件类型的命名和DOM0事件处理里的事件命名同样,第二个参数是事件函数了,使用方法的好处就是若是咱们在为同一个元素添加个点击事件,以下所示:

复制代码
btnDOM.attachEvent("onclick",function(){

         alert("Click Me!");

});

btnDOM.attachEvent("onclick",function(){

         alert("Click Me,too!");

});
复制代码

  

  运行之,两个对话框都能正常弹出来,方法让咱们能够为DOM元素添加多个不一样的点击事件。若是咱们不要某个事件呢?咱们该怎么作了,ie为删除事件提供了detachEvent方法,参数列表和attachEvent同样,若是咱们要删除某个点击事件,只要传递和添加事件同样的参数便可,以下代码所示:

btnDOM.detachEvent("onclick",function(){

         alert("Click Me,too!");

});

  

  运行之,后果很严重,咱们很迷惑,第二个click竟然没有被删除,这是怎么回事?前面我讲到删除事件要传入和添加事件同样的参数,可是在javascript的匿名函数里,两个匿名函数哪怕代码彻底同样,javascript都会在内部使用不一样变量存储,结果就是咱们看到的现象没法删除点击事件的,所以咱们的代码要这么写:

复制代码
var ftn = function(){

         alert("Click Me,too!");

};

btnDOM.attachEvent("onclick",ftn);

btnDOM.detachEvent("onclick",ftn);
复制代码

  

  这样添加的方法和删除的方法就是指向了同一个对象,因此事件删除成功了。这里的场景告诉咱们写事件要有个良好的习惯即操做函数要独立定义,不要用匿名函数用成了习惯。

接下来就是DOM2事件处理,它的原理以下图所示:

 

  DOM2是标准化的事件,使用DOM2事件,事件传递首先从捕获方式开始即从document开始,再到body,div是一个中介点,事件到了中介点时候事件就处于目标阶段,事件进入目标阶段后事件就开始冒泡处理方式,最后事件在document上结束。(捕获事件的起点以及冒泡事件的终点,我本文都是指向document,实际状况是有些浏览器会从window开始捕获,window结束冒泡,不过我以为开发时候无论浏览器自己怎么设定,咱们关注document更具开发意义,因此我这里一概都是使用document)。人们习惯把目标阶段归为冒泡的一部分,这主要是由于开发里冒泡事件使用的更加普遍。

  DOM2事件处理很折腾,每次事件促发时候都会把全部元素遍历两遍,这点和ie事件相比性能就差多了,ie只有冒泡,因此ie只须要遍历一次,不过遍历少了并不表明ie的事件体系效率更高,从开发设计角度同时支持两种事件系统会给咱们开发带来更大的灵活度,从这个角度而言DOM2事件仍是颇有可取之处。DOM2事件的代码以下:

复制代码
var btnDOM = document.getElementById("btn");

btnDOM.addEventListener("click",function(){

         alert("Click Me!");

},false);

var ftn = function(){

         alert("Click Me,too!");

};

btnDOM.addEventListener("click",ftn,false);
复制代码

  

  DOM2事件处理里添加事件使用的是addEventListener,它接收三个参数比ie事件处理多一个,前两个的意思和ie事件处理方法的两个参数同样,惟一的区别就是第一个参数里要去掉on这个前缀,第三个参数是个布尔值,若是它的取值是true,那么事件就按照捕获方式处理,取值为false,事件就是按照冒泡处理,有第三个参数咱们能够理解为何DOM2事件处理里要把事件元素跑个两遍,目的就是为了兼容两种事件模型,不过这里要请注意下,无论咱们选择是捕获仍是冒泡,两遍遍历是永远进行,若是咱们选择一种事件处理方式,那么另一个事件处理流程里就不会促发任何事件处理函数,这和汽车挂空挡空转的道理同样。经过DOM2事件方法的设计,咱们知道DOM2事件在运行时候只能执行两种事件处理方式中的一种,不可能两个事件流体系同时促发,因此虽然元素遍历两遍,可是事件函数毫不可能被促发两遍,注意我这里指不促发两遍是指一个事件函数,其实咱们能够模拟两个事件流模型同时执行的状况,例以下面代码:

btnDOM.addEventListener("click",ftn,true);

btnDOM.addEventListener("click",ftn,false);

  

  但这种写法是多事件处理,至关于咱们点击两次按钮。

  DOM2也提供了删除事件的函数,这个函数就是removeEventListener,写法以下:

btnDOM.removeEventListener("click",ftn,false);

  

  使用和ie事件的同样即参数要和定义事件的参数一致,不过removeEventListener使用时候,第三个参数不传,默认是删除冒泡事件,由于第三个参数不传默认都是false,例如:

btnDOM.addEventListener("click",ftn,true);

btnDOM.removeEventListener("click",ftn);

  

  运行之,发现事件没有被删除成功。

  最后我要说的是DOM2事件处理在ie9包括ie9以上的版本都获得了很好的支持,ie8如下是不支持DOM2事件的。

  下面咱们对三种事件方式作个比较,比较以下:

  比较一:方式一为一方和其余两种方式比较

  方式一的写法是html和javascript结合在一块儿,你中有我我中有你,把这种方式深化一下就是html和javascript混合开发,用一个软件术语表达就是代码耦合,代码耦合很差,并且是很是很差,这是菜鸟程序员的级别,因此方式一完败,另外两种方式完胜。

  比较二:方式二和方式三

  它们两个写法差很少,有时真的很难说谁好谁坏,纵观上述内容咱们发现方式二和方式三的最大区别就是:使用方式二一个DOM元素某个事件有且只有一次,而方式三则可让DOM元素某个事件拥有多个事件处理函数,在DOM2事件处理里,方式三还能让咱们精确控制事件流的方式,所以方式三的功能比方式二更加的强大,因此相比之下方式三略胜一筹。

  下面就是本文的重点:事件系统的性能问题,解决性能问题必须找到一个着力点,这里我从两个着力点来思考事件系统的性能问题,它们分别是:减小遍历次数和内存消耗。

  首先是遍历次数,无论是捕获事件流仍是冒泡事件流,都会遍历元素,而是都是从最上层的window或document开始的遍历,假如页面DOM元素父子关系很深,那么遍历的元素越多,像DOM2事件处理这种,遍历危害程度就越大了,如何解决这个事件流遍历问题了?个人回答是没有,这里有些朋友也许会有疑问,怎么会没有了?事件系统里有个事件对象即event,这个对象有阻止冒泡或捕获事件的方法,我怎么说没有呢?这位朋友的疑问颇有道理,可是若是咱们要使用该方法减小遍历,那么咱们代码就要处理父子元素的关系,爷孙元素关系,若是页面元素嵌套不少,这就是无法完成的任务,因此个人回答是无法改变遍历的问题,只能去适应它。

  看来减小遍历是无法解决事件系统性能问题了,那么如今只有从内存消耗考虑了。我常听人说C#很好用,对于web前端开发它就更好用了,咱们能够直接在C#的IDE拖一个按钮到页面,按钮到了页面以后javascript代码会自动为该按钮添加个事件,固然里面的事件函数是个空函数,因而我想咱们能够按这种方式在页面放置100个按钮,一个代码都不行就有了100个按钮事件处理,超级方便,最后咱们对其中一个按钮添加具体的按钮事件,让页面跑起来,请问你们这个页面效率会高吗?在javascript里,每一个函数都是一个对象,每一个对象都会耗费内存,因此这无用的99个事件函数代码确定消耗了不少宝贵的浏览器内存。固然现实开发环境里咱们不会这么干的,可是在当今ajax流行,单页面开发疯狂普及的时代,一个网页上的事件都是超级多的,这就意味咱们每一个事件都有一个事件函数,可是咱们每次操做都只会促发一个事件,此时其余事件都是躺着睡觉,起不到任何做用同时还要消耗计算机的内存。

  咱们须要一种方案改变这种状况,现实中的确有这种方案。为了清晰描述这个方案,我要先补充一些背景知识,在讲述DOM2事件处理里我提到了目标对象这个概念,抛开DOM2事件处理方式,在捕获事件处理和冒泡事件处理里也有目标对象的概念,目标对象就是事件具体操做的DOM元素,例如点击按钮操做里按钮就是目标对象,无论哪一个事件处理方式,事件函数都会包含一个event对象,event对象有个属性target,target是永远指向目标对象的,event对象还有个属性就是currentTarget,这个属性指向的是捕获或冒泡事件流动到的DOM元素。由上文描述咱们知道,无论是捕获事件仍是冒泡事件,事件流都会流动到document上,假如咱们在document上添加点击事件,页面上的按钮不添加点击事件,这时候咱们点击按钮,咱们知道document上的点击事件会促发,这里有个细节就是促发document点击事件时候,event的target的指向是button而不是document,那么咱们能够这样写代码:

复制代码
<input type="button" id="btn" name="btn" value="BUTTON"/>

<a href="#" id="aa">aa</a>

document.addEventListener("click",function(evt){

         var target = evt.target;

         switch(target.id){

                   case "btn":

                            alert("button");

                            break;

                   case "aa":

                            alert("a");

                            break;

         }

},false);
复制代码

 

  运行之,咱们发现效果和咱们单独写按钮事件同样。可是它的好处是不言而喻的,一个函数搞定了整个页面的事件函数,并且没有事件函数被空闲,简直完美,这个方案还有个专业名称:事件委托。jQuery的delegate方法就是按这个原理作的。其实事件委托的效率不只仅体如今事件函数的减小,它还能减小dom遍历操做,例如上面例子里咱们在document上添加函数,document是页面里的顶层对象,读取它的效率是很高的,到了具体的对象事件咱们也没有经过dom操做而是使用事件对象的target属性,全部这些只能用一句话归纳:真是快,没理由的快。

  事件委托还能给咱们带来一个很棒副产品,使用过jQuery的朋友都应该用过live方法,live方法特色是你能够为页面元素添加事件操做,哪怕这个元素目前在页面还不存在,你也能够添加它的事件,理解了事件委托机制,live的原理就很好理解了,其实jQuery的live就是经过事件委托作的,同时live仍是一种高效的事件添加方式。

  理解了事件委托,咱们会发现jQuery的bind方法是个低效的方法,由于它使用原始的事件定义方式,因此bind咱们要慎用,其实jQuery的开发者也注意到这个问题,新版的jQuery里都有一个on方法,on方法包含了bind、live和delegate方法全部功能,因此我建议看了本文的朋友要摒弃之前使用添加事件的方式,多使用on函数添加事件。

  事件委托还有个好处,上文里事件委托的例子我是在document上添加事件,这里我要作个比较,在jQuery里咱们习惯把DOM元素事件的定义放在ready方法里,以下所示:

 

$(document).ready(function(){
    XXX.bind("click",function(){});
});

   ready函数是在页面DOM文档加载完毕后执行,它比onload函数先执行,这种提早好处不少,好处之一也是带来性能提高,jQuery这种事件定义也算是个标准作法,我相信有些朋友必定又把某些事件绑定放在ready外面,最后发现按钮会无效,这种无效场景有时一刹那,过会儿就行了,因此咱们经常忽视了该问题的原理,不在ready函数绑定事件,这个操做实际上是在DOM加载完毕以前绑定事件,而这个时间段下,颇有可能某些元素还没在页面构造好,因此事件绑定会出现无效状况,所以ready定义事件的道理就是保证页面全部元素加载完毕后在定义DOM元素的事件,可是使用事件委托时能够避免问题的发生,例如将事件绑定在document,document表明整个页面,因此它加载完毕的时间可谓最先,因此在document上实现事件委托,就很难发生事件无效的状况,也很难发生浏览器报出“XXX函数未定义”的问题了。总结一下这个特色:事件委托代码能够运行在页面加载的任何阶段,这点对提高网页性能仍是加强网页效果上都会给开发人员提供更大自由度

  好了本文写毕。晚安。

相关文章
相关标签/搜索