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