關於編寫性能高效的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函數未定義」的問題了。總結一下這個特色:事件委託代碼能夠運行在頁面加載的任何階段,這點對提高網頁性能仍是加強網頁效果上都會給開發人員提供更大自由度

  好了本文寫畢。晚安。

相關文章
相關標籤/搜索