一.事件傳播機制
客戶端JavaScript程式(就是瀏覽器啦)採用了非同步事件驅動程式設計模型。當文件、瀏覽器、元素或與之相關的物件發生某些有趣的事情時,Web瀏覽器就會產生事件(event)。如果JavaScript應用程式關注特定型別的事件,那麼它可以註冊當這類事件發生時要呼叫的一個或多個函式。當然了,這種風格並非Web程式設計獨有,所有使用圖形使用者介面的應用程式都採用了它。
既然要詳解事件處理,那我們先從幾個基礎概念說起吧:
①事件型別(event type):是一個用來說明發生什麼型別事件的字串。例如,“mousemove”表示使用者移動滑鼠,“keydown”表示鍵盤上某個鍵被按下。事件型別只是一個字串,有時候又稱之為事件名字(event name);
②事件目標(event target):是發生事件或與之相關的物件。Window、Document和Element物件是最常見的事件目標。當然,AJAX中的XMLHttpRequest物件也是一個事件目標;
③事件處理程式(event handler):是處理或響應事件的函式,它也叫事件監聽程式(event listener)。應用程式通過指明事件型別和事件目標,在Web瀏覽器中註冊它們的事件處理函式。
④事件物件(event object):是與特定事件相關且包含有關該事件詳細資訊的物件。事件物件作為引數傳遞給事件處理函式(但是在IE8以及其之前版本中,全域性變數event才是事件物件)。事件物件都有用來指定事件型別(event type)的type屬性和指定事件目標(event target)的target屬性(但是在IE8以及其之前版本中,用的是srcElement而非target)。當然,不同型別的事件還會為其相關事件物件定義一些其他的獨有屬性。例如,滑鼠事件的相關物件會包含滑鼠指標的座標,而鍵盤事件的相關物件會包含按下的鍵和輔助鍵的詳細資訊。
以上說完了四個基本概念。那麼問題來了——如果在一個web頁面上用滑鼠點選一個元素a的某一子元素b時,應該先執行子元素b註冊的事件處理程式還是先執行元素a註冊的事件處理程式呢(假設元素a和它的子元素b都有註冊事件處理程式)?身為讀者的你是否想過這個問題呢?
這個問題就涉及到瀏覽器中的事件傳播(event propagation)機制。相信大家都聽說過事件冒泡(event bubble)和事件捕獲(event capturing)吧!沒錯,它們就是瀏覽器中的事件傳播機制。無圖無真相,沒有配圖?那怎麼闊以:
看了圖之後相信你已經大概理解了瀏覽器中的事件傳播機制了:當一個事件發生時,它會先從瀏覽器頂級物件Window一路向下傳遞,一直傳遞到觸發這個事件的那個元素,這也就是事件捕獲過程。然而,一切並沒有結束,事件又從這個元素一路向上傳遞到Window物件,這也就是事件冒泡過程(但是在IE8以及其之前版本中,事件模型並未定義捕獲過程,只有冒泡過程)。
所以,關於上面的問題,還得看元素a註冊的事件處理程式是在捕獲過程還是在冒泡過程了。那麼到底什麼是在捕獲過程註冊事件處理程式,在冒泡過程註冊事件處理程式又是怎麼做的呢?這就得好好說說幾種註冊事件處理程式的方式了:
1. 設定HTML標籤屬性為事件處理程式
文件元素的事件處理程式屬性,其名字由“on”後面跟著事件名組成,例如:onclick、onmouseover。當然了,這種形式只能為DOM元素註冊事件處理程式。例項:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow:hidden;} #div2{margin:50px auto; width: 200px; height: 200px; background: green; overflow:hidden;} #div3{margin:50px auto; width: 100px; height: 100px; background: blue;} </style> </head> <body> <div id="div1" onClick="console.log('div1');">div1 <div id="div2" oNClick="console.log('div2');">div2 <div id="div3" onclick="console.log('div3');" onclick="console.log('div3333');">div3 </div> </div> </div> <script type="text/javascript"> </script> </body> </html> |
結果(滑鼠點選div3區域後):
從結果中可以看出:
①因為HTML裡面不區分大小寫,所以這裡事件處理程式屬性名大寫、小寫、大小混寫均可,屬性值就是相應事件處理程式的JavaScript程式碼;
②若給同一元素寫多個onclick事件處理屬性,瀏覽器只執行第一個onclick裡面的程式碼,後面的會被忽略;
③這種形式是在事件冒泡過程中註冊事件處理程式的;
2.設定JavaScript物件屬性為事件處理程式
可以通過設定某一事件目標的事件處理程式屬性來為其註冊相應的事件處理程式。事件處理程式屬性名字由“on”後面跟著事件名組成,例如:onclick、onmouseover。例項:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow:hidden;} #div2{margin:50px auto; width: 200px; height: 200px; background: green; overflow:hidden;} #div3{margin:50px auto; width: 100px; height: 100px; background: blue;} </style> </head> <body> <div id="div1">div1 <div id="div2">div2 <div id="div3">div3 </div> </div> </div> <script type="text/javascript"> var div1 = document.getElementById('div1'); var div2 = document.getElementById('div2'); var div3 = document.getElementById('div3'); div1.onclick = function(){ console.log('div1'); }; div2.onclick = function(){ console.log('div2'); }; div3.onclick = function(){ console.log('div3'); }; div1.onclick = function(){ console.log('div11111'); }; div1.onClick = function(){ console.log('DIV11111'); }; </script> </body> </html> |
結果(滑鼠點選div3區域後):
從結果中可以看出:
①因為JavaScript是嚴格區分大小寫的,所以,這種形式下屬性名只能按規定小寫;
②若給同一元素物件寫多個onclick事件處理屬性,後面寫的會覆蓋前面的(ps:這就是在修改一個物件屬性的值,屬性的值是唯一確定的);
③這種形式也是在事件冒泡過程中註冊事件處理程式的;
3.addEventListener()
前兩種方式出現在Web初期,眾多瀏覽器都有實現。而addEventListener()方法是標準事件模型中定義的。任何能成為事件目標的物件——這些物件包括Window物件、Document物件和所有文件元素等——都定義了一個名叫addEventListener()的方法,使用這個方法可以為事件目標註冊事件處理程式。addEventListener()接受三個引數:第一個引數是要註冊處理程式的事件型別,其值是字串,但並不包括字首“on”;第二個引數是指當指定型別的事件發生時應該呼叫的函式;第三個引數是布林值,其可以忽略(某些舊的瀏覽器上不能忽略這個引數),預設值為false。這種情況是在事件冒泡過程中註冊事件處理程式。當其為true時,就是在事件捕獲過程中註冊事件處理程式。例項:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow:hidden;} #div2{margin:50px auto; width: 200px; height: 200px; background: green; overflow:hidden;} #div3{margin:50px auto; width: 100px; height: 100px; background: blue;} </style> </head> <body> <div id="div1">div1 <div id="div2">div2 <div id="div3">div3 </div> </div> </div> <script type="text/javascript"> var div1 = document.getElementById('div1'); var div2 = document.getElementById('div2'); var div3 = document.getElementById('div3'); div1.addEventListener('click', function(){ console.log('div1-bubble'); }, false); div2.addEventListener('click', function(){ console.log('div2-bubble'); }, false); div3.addEventListener('click', function(){ console.log('div3-bubble'); }, false); div3.addEventListener('click', function(){ console.log('div3-bubble222'); }, false); div1.addEventListener('click', function(){ console.log('div1-capturing'); }, true); div2.addEventListener('click', function(){ console.log('div2-capturing'); }, true); div3.addEventListener('click', function(){ console.log('div3-capturing'); }, true); </script> </body> </html> |
結果(滑鼠點選div3區域後):
從結果中可以看出:
①addEventListener()第三個引數的作用正如上面所說;
②通過addEventListener()方法給同一物件註冊多個同型別的事件,並不會發生忽略或覆蓋,而是會按順序依次執行;
相對addEventListener()的是removeEventListener()方法,它同樣有三個引數,前兩個引數自然跟addEventListener()的意義一樣,而第三個引數也只需跟相應的addEventListener()的第三個引數保持一致即可,同樣可以省略,預設值為false。它表示從物件中刪除某個事件處理函式。例項:
1 2 3 4 5 |
div1.addEventListener('click', div1BubbleFun, false); div1.removeEventListener('click', div1BubbleFun, false); function div1BubbleFun(){ console.log('div1-bubble'); } |
4.attachEvent()
但是,IE8以及其之前版本的瀏覽器並不支援addEventListener()和removeEventListener()。相應的,IE定義了類似的方法attachEvent()和detachEvent()。因為IE8以及其之前版本瀏覽器也不支援事件捕獲,所以attachEvent()並不能註冊捕獲過程中的事件處理函式,因此attachEvent()和detachEvent()要求只有兩個引數:事件型別和事件處理函式。而且,它們的第一個引數使用了帶“on”字首的事件處理程式屬性名。例項:
1 2 3 4 5 |
var div1 = document.getElementById('div1'); div1.attachEvent('onclick', div1BubbleFun); function div1BubbleFun(){ console.log('div1-bubble'); } |
相應的,從物件上刪除事件處理程式函式使用detachEvent()。例如:
1 |
div1.detachEvent('onclick', div1BubbleFun); |
到此為止,我們已經說了瀏覽器中事件傳播機制以及各種註冊事件處理程式的方法。下面我們就再說說事件處理程式呼叫時的一些問題吧!
二.事件處理程式的呼叫
1.事件處理程式的引數:正如前面所說,通常事件物件作為引數傳遞給事件處理函式,但IE8以及其之前版本的瀏覽器中全域性變數event才是事件物件。所以,我們在寫相關程式碼時應該注意相容性問題。例項(給頁面上id為div1的元素新增點選事件,當點選該元素時在控制檯輸出事件型別和被點選元素本身):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow: hidden;} </style> </head> <body> <div id="div1">div1</div> <script type="text/javascript"> var div1 = document.getElementById('div1'); if(div1.addEventListener){ div1.addEventListener('click', div1Fun, false); }else if(div1.attachEvent){ div1.attachEvent('onclick', div1Fun); } function div1Fun(event){ event = event || window.event; var target = event.target || event.srcElement; console.log(event.type); console.log(target); } </script> </body> </html> |
2.事件處理程式的執行環境:關於事件處理程式的執行環境,也就是在事件處理程式中呼叫上下文(this值)的指向問題,可以看下面四個例項。
例項一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow: hidden;} </style> </head> <body> <div id="div1" onclick="console.log('html:'); console.log(this);">div1</div> <script type="text/javascript"> </script> </body> </html> |
結果一:
從結果可以看出:
①第一種方法事件處理程式中this指向這個元素本身;
例項二:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow: hidden;} </style> </head> <body> <div id="div1" onclick="console.log('html:'); console.log(this);">div1</div> <script type="text/javascript"> var div1 = document.getElementById('div1'); div1.onclick = function(){ console.log('div1.onclick:'); console.log(this); }; </script> </body> </html> |
結果二:
從結果可以看出:
①第二種方法事件處理程式中this也指向這個元素本身;
②存在第二種方法時,它會覆蓋第一種方法註冊的事件處理程式;
例項三:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow: hidden;} </style> </head> <body> <div id="div1" onclick="console.log('html:'); console.log(this);">div1</div> <script type="text/javascript"> var div1 = document.getElementById('div1'); div1.onclick = function(){ console.log('div1.onclick:'); console.log(this); }; div1.addEventListener('click', function(){ console.log('div1.addEventListener:'); console.log(this); }, false); </script> </body> </html> |
結果三:
從結果可以看出:
①第三種方法事件處理程式中this也指向這個元素本身;
②第三種方法並不會覆蓋第一種或第二種方法註冊的事件處理程式;
例項四:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow: hidden;} </style> </head> <body> <div id="div1" onclick="console.log('html:'); console.log(this);">div1</div> <script type="text/javascript"> var div1 = document.getElementById('div1'); div1.onclick = function(){ console.log('div1.onclick:'); console.log(this); }; div1.attachEvent('onclick', function(){ console.log('div1.attachEvent:'); console.log(this === window); }); </script> </body> </html> |
結果四:
從結果可以看出:
①第四種方法事件處理程式中this指向全域性物件Window;
②第四種方法也不會覆蓋第一種或第二種方法註冊的事件處理程式;
3.事件處理程式的呼叫順序:多個事件處理程式呼叫規則如下:
①通過HTML屬性註冊的處理程式和通過設定物件屬性的處理程式一直優先呼叫;
②使用addEventListener()註冊的處理程式按照它們的註冊順序依次呼叫;
③使用attachEvent()註冊的處理程式可能按照任何順序呼叫,所以程式碼不應該依賴於呼叫順序;
4.事件取消:
①取消事件的瀏覽器預設操作(比如點選超連結元素會自動發生頁面跳轉的預設操作):如果使用前兩種方法註冊事件處理程式,可以在處理程式中新增返回值false來取消事件的瀏覽器預設操作。在支援addEventListener()的瀏覽器中,也可以通過呼叫事件物件的preventDefault()方法取消事件的預設操作。至於IE8及其之前的瀏覽器可以通過設定事件物件的returnValue屬性為false來取消事件的預設操作。參考程式碼:
1 2 3 4 5 6 7 8 9 10 |
function cancelHandler(event){ var event = event || window.event; if(event.preventDefault){ event.preventDefault(); } if(event.returnValue){ event.returnValue = false; } return false; } |
②取消事件傳播:在支援addEventListener()的瀏覽器中,可以呼叫事件物件的一個stopPropagation()方法阻止事件的繼續傳播,它能工作在事件傳播期間的任何階段(捕獲期階段、事件目標本身、冒泡階段);但是在IE8以及其之前版本的瀏覽器中並不支援stopPropagation()方法,而且這些瀏覽器也不支援事件傳播的捕獲階段,相應的,IE事件物件有一個cancelBubble屬性,設定這個屬性為true能阻止事件進一步傳播(即阻止其冒泡)。參考程式碼(阻止發生在div3區域的點選事件冒泡到div2和div1):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>test</title> <style type="text/css"> #div1{width: 300px; height: 300px; background: red; overflow:hidden;} #div2{margin:50px auto; width: 200px; height: 200px; background: green; overflow:hidden;} #div3{margin:50px auto; width: 100px; height: 100px; background: blue;} </style> </head> <body> <div id="div1">div1 <div id="div2">div2 <div id="div3">div3 </div> </div> </div> <script type="text/javascript"> var div1 = document.getElementById('div1'); var div2 = document.getElementById('div2'); var div3 = document.getElementById('div3'); div1.onclick = function(){ console.log('div1'); }; div2.onclick = function(){ console.log('div2'); }; div3.onclick = function(event){ stopEventPropagation(event); console.log('div3'); }; function stopEventPropagation(event){ var event = event || window.event; if(event.stopPropagation){ event.stopPropagation(); }else{ event.cancelBubble = true; } } </script> </body> </html> |
當然,關於事件冒泡還是有可利用之處的,這也就是我們常說的事件代理或者事件委託。