如何能做出高效率的web前端程式是我每次做前端開發都會不自覺去考慮的問題。幾年前雅虎裡牛逼的前端工程師們出了一本關於提升web前端性能的書籍,轟動了整個web開發技術界,讓神秘的web前端優化問題成為了大街的白菜,web前端優化變成了菜鳥和大牛都能回答的簡單問題,當整個業界都知道了驚天秘密的答案,那麼現有的優化技術已經不能對你開發的網站產生的質的飛越,為了讓我們開發的網站性能比別人的網站更加優秀,我們需要更深入的獨立思考,儲備更優秀的技能。
Javascript裡的事件系統是我想到的第一個突破點。為什麼會是javascript的事件系統呢?我們都知道web前端包含三個技術:html、css和javascript,html和css如何結合真是一目了然:style、class、id以及html標籤,這個沒啥好講的,但是javascript是如何切入到html和css中間,讓三者融合呢?最後我發現這個切入點就是javascript的事件系統,不管我們寫多長多複雜的javascript程式碼,最終都是透過事件系統體現在html和css上,所以我就在想既然事件系統是三者融合的切入點,那麼一個頁面裡,特別是當今越來越複雜的網頁裡必然會有大量事件操作,沒有這些事件我們精心編寫的javascript程式碼只有刀槍入庫,英雄無用武之地了。既然頁面會存在大量事件函數,那麼我們依習慣寫事件函數,會存在影響效率的問題嗎?我研究下來的答案是真有效率問題,而且還是嚴重的效率問題。
為了說清楚我的答案,我要先詳細講解下javascript的事件系統。
事件系統是javascript和html以及css融合的切入點,這個切人點好比java裡的main函數,一切神奇都是由這裡開始,那麼瀏覽器是如何完成這種切入呢?我研究下來一共有3種方式,它們分別是:
方式一:html#事件處理
html事件處理就是將事件函數直接寫在html標籤裡,因為這種寫法和html標籤緊密耦合,所以稱為html事件處理。例如下面程式碼:
<input type="button" id="btn" name="btn" onclick="alert('Click Me!')"/>
如果click事件函數複雜了,這麼寫程式碼一定會帶來不便,因此我們常常把函數寫在外部,onclick直接呼叫函數名,例如:
<input type="button" id="btn" name="btn" onclick="btnClk()"/> function btnClk(){ alert("click me!"); }
上面這個寫法是一種很美的寫法,所以時下還是很多人會不自覺的使用它,但是也許很多人不知道,後一種寫法其實沒有前一種寫法健壯,這個也是我前不久在研究非阻塞載入腳本技術時候碰到的問題,因為根據前端優化的原則,javascript程式碼往往是位於頁面的底部,當頁面有被腳本阻塞時候,html標籤裡引用的函式可能還沒執行到,這個時候我們點擊頁面按鈕,結果會報出“XXX函數未定義的錯誤”,在javascript裡這樣的錯誤是會被try,catch所捕獲,因此為了讓程式碼更加健壯,我們會有如下的改寫:
<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表示,页面里有个p,p等于是覆盖在document元素上,p里面有个button元素,button元素是覆盖在p上,也等于覆盖着document上,所以问题来了,当我们点击这个按钮时候,这个点击行为其实不仅仅发生在button之上,p和document都被作用了点击操作,按逻辑这三个元素都是可以促发点击事件的,而事件流正是描述上述场景的概念,事件流的意思是:从页面接收事件的顺序。
第二个概念:事件冒泡和事件捕获
事件冒泡是微软公司提出解决事件流问题的方案,而事件捕获则是网景公司提出的事件流解决方案,它们的原理如下图:
冒泡事件由p开始,其次是body,最后是document,事件捕获则是倒过来的先是document,其次是body,最后是目标元素p,相比之下,微软公司的方案更加人性化符合人们的操作习惯,网景的方案就很别扭了,这是浏览器大战的恶果,网景慢了一步就以牺牲用户习惯的代码解决事件流的问题。
微软公司结合冒泡事件设计了一套新的事件系统,业界习惯称为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,p是一个中介点,事件到了中介点时候事件就处于目标阶段,事件进入目标阶段后事件就开始冒泡处理方式,最后事件在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函数未定义”的问题了。总结一下这个特点:事件委托代码可以运行在页面加载的任何阶段,这点对提升网页性能还是增强网页效果上都会给开发人员提供更大自由度。
以上是JavaScript事件高效能編寫的實例程式碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!