這篇文章對於瞭解Javascript的事件處理機制非常好,將它全文轉載於此,以備不時之需。
什麼是事件?
事件(Event)是JavaScript應用跳動的心臟 ,也是把所有東西粘在一起的膠水。當我們與瀏覽器中 Web 頁面進行某些型別的互動時,事件就發生了。事件可能是使用者在某些內容上的點選、滑鼠經過某個特定元素或按下鍵盤上的某些按鍵。事件還可能是 Web 瀏覽器中發生的事情,比如說某個 Web 頁面載入完成,或者是使用者滾動視窗或改變視窗大小。
通過使用 JavaScript ,你可以監聽特定事件的發生,並規定讓某些事件發生以對這些事件做出響應。
今天的事件
在漫長的演變史,我們已經告別了內嵌式的事件處理方式(直接將事件處理器放在 HTML 元素之內來使用)。今天的事件,它已是DOM的重要組成部分,遺憾的是, IE繼續保留它最早在IE4.0中實現的事件模型,以後的IE版本中也沒有做太大的改變,這也就是說IE還是使用的是一種專有的事件模型(冒泡型),而其它的主流瀏覽器直到DOM 級別 3 規定定案後,才陸陸續續支援DOM標準的事件處理模型 — 捕獲型與冒泡型。
歷史原因是:W3C 規範 在DOM 級別 1中並沒有定義任何的事件,直到釋出於 2000 年 11 月 的DOM 級別 2 才定義了一小部分子集,DOM 級別 2中已經提供了提供了一種更詳細的更細緻的方式以控制 Web 頁面中的事件,最後,完整的事件是在2004年 DOM 級別 3的規定中才最終定案。因為IE4是1995推出的並已實現了自己的事件模型(冒泡型),當時根本就沒有DOM標準,不過在以後的DOM標準規範過程中已經把IE的事件模型吸收到了其中。
目前除IE瀏覽器外,其它主流的Firefox, Opera, Safari都支援標準的DOM事件處理模型。IE仍然使用自己專有的事件模型,即冒泡型,它事件模型的一部份被DOM標準採用,這點對於開發者來說也是有好處的,只有使用 DOM標準,IE都共有的事件處理方式才能有效的跨瀏覽器。
DOM事件流
DOM(文件物件模型)結構是一個樹型結構,當一個HTML元素產生一個事件時,該事件會在元素結點與根節點之間按特定的順序傳播,路徑所經過的節點都會收到該事件,這個傳播過程可稱為DOM事件流。
事件順序有兩種型別:事件捕捉和事件冒泡。
冒泡型事件(Event Bubbling)
這是IE瀏覽器對事件模型的實現,也是最容易理解的,至少筆者覺得比較符合實際的。冒泡,顧名思義,事件像個水中的氣泡一樣一直往上冒,直到頂端。從 DOM樹型結構上理解,就是事件由葉子節點沿祖先結點一直向上傳遞直到根節點;從瀏覽器介面檢視HTML元素排列層次上理解就是事件由具有從屬關係的最確定的目標元素一直傳遞到最不確定的目標元素.冒泡技術.冒泡型事件的基本思想,事件按照從特定的事件目標開始到最不確定的事件目標.
捕獲型事件(Event Capturing)
Netscape 的實現,它與冒泡型剛好相反,由DOM樹最頂層元素一直到最精確的元素,這個事件模型對於開發者來說(至少是我..)有點費解,因為直觀上的理解應該如同冒泡型,事件傳遞應該由最確定的元素,即事件產生元素開始。
DOM標準的事件模型
我們已經對上面兩個不同的事件模型進行了解釋和對比。DOM標準同時支援兩種事件模型,即捕獲型事件與冒泡型事件,但是,捕獲型事件先發生。兩種事件流都會觸發DOM中的所有物件,從document物件開始,也在document物件結束(大部分相容標準的瀏覽器會繼續將事件是捕捉/冒泡延續到window物件)。
如圖:首先是捕獲式傳遞事件,接著是冒泡式傳遞,所以,如果一個處理函式既註冊了捕獲型事件的監聽,又註冊冒泡型事件監聽,那麼在DOM事件模型中它就會被呼叫兩次。
DOM標準的事件模型最獨特的性質是,文字節點也會觸發事件(在IE不會)。
事件傳送
為了更好的說明DOM標準中的事件流原理,我們把它放在“事件傳送”小結裡來更具體的解釋。
顯然,如果為一個超連結添加了click事件監聽器,那麼當該連結被點選時該事件監聽器就會被執行。但如果把該事件監聽器指派給了包含該連結的p元素或者位於DOM樹頂端的document節點,那麼點選該連結也同樣會觸發該事件監聽器。這是因為事件不僅僅對觸發的目標元素產生影響,它們還會對沿著DOM結構的所有元素產生影響。這就是大家所熟悉的事件轉送。
W3C事件模型中明確地指出了事件轉送的原理。事件傳送可以分為3個階段。
如圖:標準的事件轉送模式
(1).在事件捕捉(Capturing)階段,事件將沿著DOM樹向下轉送,目標節點的每一個祖先節點,直至目標節點。例如,若使用者單擊了一個超連結,則該單擊事件將從document節點轉送到html元素,body元素以及包含該連結的p元素。
在此過程中,瀏覽器都會檢測針對該事件的捕捉事件監聽器,並且執行這件事件監聽器。
(2). 在目標(target)階段,瀏覽器在查詢到已經指定給目標事件的事件監聽器之後,就會執行 該事件監聽器。目標節點就是觸發事件的DOM節點。例如,如果使用者單擊一個超連結,那麼該連結就是目標節點(此時的目標節點實際上是超連結內的文字節點)。
(3).在冒泡(Bubbling)階段,事件將沿著DOM樹向上轉送,再次逐個訪問目標元素的祖先節點到document節點。該過程中的每一步。瀏覽器都將檢測那些不是捕捉事件監聽器的事件監聽器,並執行它們。
並非所有的事件都會經過冒泡階段的
所有的事件都要經過捕捉階段和目標階段,但是有些事件會跳過冒泡階段。例如,讓元素獲得輸入焦點的focus事件以及失去輸入焦點的blur事件就都不會冒泡。
事件控制代碼和事件接聽器
事件控制代碼
事件控制代碼(又稱事件處理函式,DOM稱之為事件監聽函式),用於響應某個事件而呼叫的函式稱為事件處理函式 。每一個事件均對應一個事件控制代碼,在程式執行時,將相應的函式或語句指定給事件控制代碼,則在該事件發生時,瀏覽器便執行指定的函式或語句,從而實現網頁內容與使用者操作的互動。當瀏覽器檢測到某事件發生時,便查詢該事件對應的事件控制代碼有沒有被賦值,如果有,則執行該事件控制代碼。
我們認為響應點選事件的函式是onclick事件處理函式。以前,事件處理函式有兩種分配方式:在JavaScript中或者在HTML中。
如果在JavaScript 中分配事件處理函式, 則需要首先獲得要處理的物件的一引用,然後將函式賦值給對應的事件處理函式屬性,請看一個簡單的例子:
1 var link=document.getElementById("mylink");2 link.onclick=function(){3 alert("I was clicked !");4 };
從我們看到的例子中,我們發現使用事件控制代碼很容易, 不過事件處理函式名稱必須是小寫的,還有就是隻有在 元素載入完成之後才能將事件控制代碼賦給元素,不然會有異常。
如果在HTML中分配事件控制代碼的話,則直接通過HTML屬性來設定事件處理函式就行了,並在其中包含合適的指令碼作為特性值就可以了,例如:
<a href="/" onclick="JavaScript code here">......</a>
......</a>
這種JavaScript 程式碼和通過HTML的style屬性直接將CSS屬性賦給元素類似。這樣會程式碼看起來一團糟,也違背了將實現動態行為的程式碼與顯示文件靜態內容的程式碼相分離的原則。從1998年開始,這種寫法就過時了。
這種傳統的事件繫結技術,優缺點是顯然的:
*簡單方便,在HTML中直接書寫處理函式的程式碼塊,在JS中給元素對應事件屬性賦值即可。
*IE與DOM標準都支援的一種方法,它在IE與DOM標準中都是在事件冒泡過程中被呼叫的。
*可以在處理函式塊內直接用this引用註冊事件的元素,this引用的是當前元素。
*要給元素註冊多個監聽器,就不能用這方法了。
事件接聽器
除了前面已經介紹的簡單事件控制代碼之外,現在大多數瀏覽器都內建了一些更高階的事件處理方式,即,事件監聽器,這種處理方式就不受一個元素只能繫結一個事件控制代碼的限制。
我們已經知道了事件控制代碼與事件監聽器的最大不同之處是使用事件控制代碼時一次只能插接一個事件控制代碼,但對於事件監聽器,一次可以插接多個。
IE下的事件監聽器:
IE提供的卻是一種自有的,完全不同的甚至存在BUG的事件監聽器,因此如果要讓指令碼在本瀏覽器中正常執行的話,就必須使用IE所支援的事件監聽器。另外,Safari 瀏覽器中的事件監聽器有時也存在一點不同。
在IE中,每個元素和window物件都有兩個方法:attachEvent方法和detachEvent方法。
1 element.attachEvent("onevent",eventListener);
此方法的意思是在IE中要想給一個元素的事件附加事件處理函式,必須呼叫attachEvent方法才能建立一個事件監聽器。attachEvent方法允許外界註冊該元素多個事件監聽器。
attachEvent接受兩個引數。第一個引數是事件型別名,第二個引數eventListener是回撥處理函式。這裡得說明一下,有個經常會出錯的地方,IE下 利用attachEvent註冊的處理函式呼叫時this指向不再是先前註冊事件的元素,這時的this為window物件。還有一點是此方法的事件型別名稱必須加上一個”on”的字首(如onclick)。
1 element.attachEvent("onevent",eventListener);
要想移除先前元素註冊的事件監聽器,可以使用detachEvent方法進行刪除,引數相同。
DOM標準下的事件監聽器:
在支援W3C標準事件監聽器的瀏覽器中,對每個支援事件的物件都可以使用addEventListener方法。該方法既支援註冊冒泡型事件處理,又支援捕獲型事件處理。所以與IE瀏覽器中註冊元素事件監聽器方式有所不同的。
1 //標準語法 2 element.addEventListener('event', eventListener, useCapture);3 //預設4 element.addEventListener('event', eventListener, false);
addEventListener方法接受三個引數。第一個引數是事件型別名,值得注意的是,這裡事件型別名稱與IE的不同,事件型別名是沒’on’開頭的;第二個引數eventListener是回撥處理函式(即監聽器函式);第三個引數註明該處理回撥函式是在事件傳遞過程中的捕獲階段被呼叫還是冒泡階段被呼叫 ,通常此引數通常會設定為false(為false時是冒泡),那麼,如果將其值設定為true,那就建立一個捕捉事件監聽器。
移除已註冊的事件監聽器呼叫element的removeEventListener方法即可,引數相同。
1 //標準語法 2 element.removeEventListener('event', eventListener, useCapture);3 //預設4 element.removeEventListener('event', eventListener, false);
通過addEventListener方法新增的事件處理函式,必須使用removeEventListener方法才能刪除,而且要求引數與新增事件處理函式時addEventListener方法的引數完全一致(包括useCapture引數),否則將不能成功刪除事件處理函式。
跨瀏覽器的註冊與移除元素事件監聽器方案
我們現在已經知道,對於支援addEventListener方法的瀏覽器,只要需要事件監聽器指令碼就都需要呼叫addEventListener方法;而對於不支援該方法的IE瀏覽器,使用事件監聽器時則需要呼叫attachEvent方法。要確保瀏覽器使用正確的方法其實並不困難,只需要通過一個if-else語句來檢測當前瀏覽器中是否存在addEventListener方法或attachEvent方法即可。
這樣的方式就可以實現一個跨瀏覽器的註冊與移除元素事件監聽器方案:
1 var EventUtil = { 2 //註冊 3 addHandler: function(element, type, handler){ 4 if (element.addEventListener){ 5 element.addEventListener(type, handler, false); 6 } else if (element.attachEvent){ 7 element.attachEvent("on" + type, handler); 8 } else { 9 element["on" + type] = handler;10 }11 },12 //移除註冊13 removeHandler: function(element, type, handler){14 if (element.removeEventListener){15 element.removeEventListener(type, handler, false);16 } else if (element.detachEvent){17 element.detachEvent("on" + type, handler);18 } else {19 element["on" + type] = null;20 }21 } 22 };
事件物件引用
為了更好的處理事件,你可以根據所發生的事件的特定屬性來採取不同的操作。
如事件模型一樣,IE 和其他瀏覽器處理方法不同:IE 使用一個叫做 event 的全域性事件物件來處理物件(它可以在全域性變數window.event中找到),而其它所有瀏覽器採用的 W3C 推薦的方式,則使用獨立的包含事件物件的引數傳遞。
跨瀏覽器實現這樣的功能時,最常見的問題就是獲取事件本身的引用及獲取該事件的目標元素的引用。
下面這段程式碼就為你解決了這個問題:
1 var EventUtil ={2 getEvent: function(event){3 return event ? event : window.event;4 },5 getTarget: function(event){6 return event.target || event.srcElement;7 }8 };
停止事件冒泡和阻止事件的預設行為
“停止事件冒泡“和”阻止瀏覽器的預設行為“,這兩個概念非常重要,它們對複雜的應用程式處理非常有用。
1.停止事件冒泡
停止事件冒泡是指,停止冒泡型事件的進一步傳遞(取消事件傳遞,不只是停止IE和DOM標準共有的冒泡型事件,我們還可以停止支援DOM標準瀏覽器的捕捉型事件,用topPropagation()方法)。例如上圖中的冒泡型事件傳遞中,在body處理停止事件傳遞後,位於上層的document的事件監聽器就不再收到通知,不再被處理。
2.阻止事件的預設行為
停止事件的預設行為是指,通常瀏覽器在事件傳遞並處理完後會執行與該事件關聯的預設動作(如果存在這樣的動作)。例如,如果表單中input type 屬性是 “submit”,點選後在事件傳播完瀏覽器就自動提交表單。又例如,input 元素的 keydown 事件發生並處理後,瀏覽器預設會將使用者鍵入的字元自動追加到 input 元素的值中。
停止事件冒泡的處理方法:
在IE下,通過設定event物件的cancelBubble為true即可。
1 function someHandle() {2 window.event.cancelBubble = true;3 }
DOM標準通過呼叫event物件的stopPropagation()方法即可。
1 function someHandle(event) {2 event.stopPropagation();3 }
因些,跨瀏覽器的停止事件傳遞的方法是:
1 function someHandle(event) {2 event = event || window.event;3 if(event.stopPropagation){4 event.stopPropagation();5 }else {6 event.cancelBubble = true;7 }8 }
阻止事件的預設行為的處理方法
就像事件模型和事件物件差異一樣,在IE和其它所有瀏覽器中阻止事件的預設行為的方法也不同。
在IE下,通過設定event物件的returnValue為false即可。
1 function someHandle() {2 window.event.returnValue = false;3 }
DOM標準通過呼叫event物件的preventDefault()方法即可。
1 function someHandle(event) {2 event.preventDefault();3 }
因些,跨瀏覽器的取消事件傳遞後的預設處理方法是:
1 function someHandle(event) {2 event = event || window.event;3 if(event.preventDefault){4 event.preventDefault();5 }else{6 event.returnValue = false;7 }8 }
完整的事件處理相容性函式
1 var EventUtil = { 2 addHandler: function(element, type, handler){ 3 if (element.addEventListener){ 4 element.addEventListener(type, handler, false); 5 } else if (element.attachEvent){ 6 element.attachEvent("on" + type, handler); 7 } else { 8 element["on" + type] = handler; 9 }10 },11 removeHandler: function(element, type, handler){12 if (element.removeEventListener){13 element.removeEventListener(type, handler, false);14 } else if (element.detachEvent){15 element.detachEvent("on" + type, handler);16 } else {17 element["on" + type] = null;18 }19 },20 getEvent: function(event){21 return event ? event : window.event;22 },23 getTarget: function(event){24 return event.target || event.srcElement;25 },26 preventDefault: function(event){27 if (event.preventDefault){28 event.preventDefault();29 } else {30 event.returnValue = false;31 }32 },33 stopPropagation: function(event){34 if (event.stopPropagation){35 event.stopPropagation();36 } else {37 event.cancelBubble = true;38 }39 };
捕獲型事件模型與冒泡型事件模型的應用場合
標準事件模型為我們提供了兩種方案,可能很多朋友分不清這兩種不同模型有啥好處,為什麼不只採取一種模型。 這裡拋開IE瀏覽器討論(IE只有一種,沒法選擇)什麼情況下適合哪種事件模型。
1. 捕獲型應用場合
捕獲型事件傳遞由最不精確的祖先元素一直到最精確的事件源元素,傳遞方式與作業系統中的全域性快捷鍵與應用程式快捷鍵相似。當一個系統組合鍵發生時,如果注 冊了系統全域性快捷鍵監聽器,該事件就先被作業系統層捕獲,全域性監聽器就先於應用程式快捷鍵監聽器得到通知,也就是全域性的先獲得控制權,它有權阻止事件的進 一步傳遞。所以捕獲型事件模型適用於作全域性範圍內的監聽,這裡的全域性是相對的全域性,相對於某個頂層結點與該結點所有子孫結點形成的集合範圍。
例如你想作全域性的點選事件監聽,相對於document結點與document下所有的子結點,在某個條件下要求所有的子結點點選無效,這種情況下冒泡模型就解決不了了,而捕獲型卻非常適合,可以在最頂層結點新增捕獲型事件監聽器,偽碼如下:
1 function globalClickListener(event) {2 if(canEventPass == false) {3 //取消事件進一步向子結點傳遞和冒泡傳遞4 event.stopPropagation();5 //取消瀏覽器事件後的預設執行6 event.preventDefault();7 }8 }
這樣一來,當canEventPass條件為假時,document下所有的子結點click註冊事件都不會被瀏覽器處理。
2. 冒泡型的應用場合
可以說我們平時用的都是冒泡事件模型,因為IE只支援這模型。這裡還是說說,在恰當利用該模型可以提高指令碼效能。在元素一些頻繁觸發的事件中,如 onmousemove, onmouseover,onmouseout,如果明確事件處理後沒必要進一步傳遞,那麼就可以大膽的取消它。此外,對於子結點事件監聽器的處理會對父 層監聽器處理造成負面影響的,也應該在子結點監聽器中禁止事件進一步向上傳遞以消除影響。
綜合案例分析
最後結合下面HTML程式碼作分析:
1 <body onclick="alert('current is body');"> 2 <div id="div0" onclick="alert('current is '+this.id)"> 3 <div id="div1" onclick="alert('current is '+this.id)"> 4 <div id="div2" onclick="alert('current is '+this.id)"> 5 <div id="event_source" onclick="alert('current is '+this.id)" style="height:200px;width:200px;background-color:red;"></div> 6 </div> 7 </div> 8 </div> 9 </body>10
HTML執行後點擊紅色區域,這是最裡層的DIV,根據上面說明,無論是DOM標準還是IE,直接寫在html裡的監聽處理函式是事件冒泡傳遞時呼叫的,由最裡層一直往上傳遞,所以會先後出現 current is event_source current is div2 current is div1 current is div0 current is body
新增以下片段:
1 var div2 = document.getElementById('div2');2 EventUtil.addHandler(div2, 'click', function(event){3 event = EventUtil.getEvent(event);4 EventUtil.stopPropagation(event);5 }, false);
current is event_sourcecurrent is div2
當點選紅色區域後,根據上面說明,在泡冒泡處理期間,事件傳遞到div2後被停止傳遞了,所以div2上層的元素收不到通知,所以會先後出現:
在支援DOM標準的瀏覽器中,新增以下程式碼:
1 document.body.addEventListener('click', function(event){2 event.stopPropagation();3 }, true);
以上程式碼中的監聽函式由於是捕獲型傳遞時被呼叫的,所以點選紅色區域後,雖然事件源是ID為event_source的元素,但捕獲型選傳遞,從最頂層開始,body結點監聽函式先被呼叫,並且取消了事件進一步向下傳遞,所以只會出現 current is body .