http://developer.apple.com.cn/internet/webcontent/eventmodels.html
事件使得客戶端的 JavaScript 有機會被激活,并得以運行。在一個 Web 頁面裝載之后,運行腳本的唯一方式,就是響應系統或者用戶的動作。雖然從第一個支持腳本編程的瀏覽器面世以來,簡單的事件被實現為 JavaScript 的一部分;但是大多數最近出現的瀏覽器都實現了強壯的事件模型,使腳本可以更加智能地處理事件?,F在的問題在于:為了支持各種瀏覽器,您必須和多個先進的事件模型做斗爭,準確地說,是三個。
這三個事件模型分別和下面的文檔對象模型(Document Object Model,即 DOM)三巨頭結盟:Netscape Navigator 4 (NN4),Macintosh 和 Windows 系統的 Internet Explorer 4 及其更新版本(IE4+),以及在 Safari 中得到實現的 W3C DOM。盡管這些模型之間有些地方存在一些本質的差別,但是在一些簡易的 JavaScript 的幫助下,它們都可以同時適用于同一個文檔。本文主要著眼于相互沖突的事件模型中的兩個關鍵方面:
- 把一個事件和 HTML 元素綁定起來的方法。
- 在事件被觸發后如何對之進行處理。
事件綁定的方法
事件綁定是指構造一個響應系統或者用戶動作的 HTML 元素的過程。在不同的瀏覽器版本中,有不少于五種事件綁定技術。下面我們快速地介紹一下這些技術。
事件綁定方法I:綁定元素屬性
最簡單和向后兼容性最好的事件綁定方法是把事件綁定到元素標識的屬性。事件屬性名稱由事件類型外加一個“on”前綴構成。盡管HTML屬性并不是大小寫敏感的,人們還是定義了一個規則,規定事件類型的每一個“詞”的首字母大寫,比如 onClick
和 onMouseOver
。這些屬性也被稱為事件處理器,因為它們指示了元素如何“處理”特定的事件類型。
正確的事件處理器屬性的值在形式上是被引號包含的 JavaScript 語句。最常見的值是一條調用某個腳本函數的語句,而被調用的函數在位于文檔前部的 <SCRIPT> 標識中定義--該標識通常位于 <HEAD> 部分。舉例來說,下面的函數:
function myFunc() {
// script statements here
}
可以被定義為一個按鍵控件的事件處理器,按鍵的定義如下:
<INPUT TYPE="button" NAME="myButton" VALUE="Click Here"
onClick="myFunc()">
把事件綁定到元素屬性上有一個優點,即可以支持開發者把參數傳遞給事件處理器函數。接收事件的元素的引用則由一個特殊的參數值--this
關鍵字來傳遞。下面的代碼演示一個函數如何借助傳入參數,把任意數目的文本框的內容轉化為大寫:
<SCRIPT LANGUAGE="JavaScript">
function convertToUpper(textbox) {
textbox.value = textbox.value.toUpperCase();
}
</SCRIPT>
...
<FORM ....>
<INPUT TYPE="text" NAME="first_name" onChange="convertToUpper(this)">
<INPUT TYPE="text" NAME="last_name" onChange="convertToUpper(this)">
...
</FORM>
事件綁定方法II:綁定對象屬性
對于 NN3+ 和 IE4+ 這兩類瀏覽器,腳本編程人員可以以腳本語句的方式把事件綁定到對象上,而不是綁定到元素標識的屬性上。每一個負責事件響應的元素對象都為自己能夠識別的事件設置了相應的屬性。對象屬性名稱是元素標識屬性的小寫形式,比如 onmouseover
。NN4 還接受 interCap(即首字小寫,之后的每一個詞的首字大寫)版本的屬性名,但是考慮到跨瀏覽器的兼容性,所有字母都是小寫的名稱會更安全一些。
當您把一個函數的引用賦值給一個事件屬性的時候,就發生了綁定。函數的引用是指函數的名稱,但是不帶函數定義中的括號。因此,如果要為一個名為 myButton
的按鍵的點擊事件(click)進行綁定,使之激活一個定義為 myFunc()
的函數,則其賦值語句如下所示:
document.forms[0].myButton.onclick = myFunc;
您應該注意一點:在事件觸發的時候,沒有辦法向事件函數傳遞參數。本文在稍候對事件處理過程的討論中還會回顧這個問題。
事件綁定方法III: 綁定 IE4+<SCRIPT FOR> 標識
在 IE4+ 中,Microsoft 對 <SCRIPT> 標識實現了自己的擴展,可以將它包含的腳本語句和某個元素的一個事件類型進行綁定。支持這個綁定的標識屬性(還沒有被 W3C 批準為 HTML 的一部分)是 FOR
和 EVENT
。
FOR
屬性的值必須是您為元素的 ID 屬性分配的唯一標識符。然后,您必須把事件的名稱(onmouseover,onclick,等等)分配給 EVENT
屬性。在上面的按鍵實例的基礎上,我們必須對按鍵標識進行修改,使之包含一個 ID
屬性:
<INPUT TYPE="button" NAME="myButton" ID="button1" VALUE="Click Here">
腳本語句并不在函數中,而是在 <SCRIPT> 標識中,如下所示:
<SCRIPT FOR="button1" EVENT="onclick">
// script statements here
</SCRIPT>
當然,標識中的語句可以調用頁面上其它地方定義的任何函數(或者從.js文件中導入的函數)。然而,這種綁定方式意味著您必須為每一個元素和每一個事件創建一個 <SCRIPT FOR> 標識。
您還必須小心,只能把這種綁定方法部署在僅供 IE4+ 瀏覽器瀏覽的頁面。其它任何支持腳本編程而又沒有實現這個特殊的 <SCRIPT> 標識的瀏覽器(包括 IE3),都將把它作為常規的 <SCRIPT> 標識來處理,并試圖在頁面裝載的時候執行這些腳本語句--這不可避免地引起腳本錯誤。
事件綁定方法IV:使用 IE5/Windows 的 attachEvent() 方法
早在 W3C DOM 工作組磨礪出標準的事件模型之前,attachEvent()
方法已經被實現了,并且可被用于 Windows 版的 IE5 或更新版本的瀏覽器上的每一個 HTML 元素。
attachEvent()
方法的用法如下所示:
elemObject.attachEvent("eventName", functionReference);
eventName 參數的值是表示事件名稱的字符串,比如 onmousedown
。functionReference 參數是一個不帶括號的函數引用,和早些時候描述的事件屬性方法中一樣。因此對于上面例子的按鍵對象,可以通過如下的腳本語句把函數綁定到按鍵的 click 事件:
document.getElementById("button1").attachEvent("onclick", myFunc);
由于 attachEvent()
方法必須嚴格工作在 IE5+/Windows 的環境中,所以您既可以使用 W3C DOM 的元素引用方式(如上文所示),也可以使用 IE4+ 的引用方式:
document.all.button1.attachEvent("onclick", myFunc);
這個方法有一個值得注意的地方:您不能在元素被載入瀏覽器之前執行這個語句。該對象的引用在相應的 HTML 按鍵元素被瀏覽器創建之前,都是無效的。因此,要讓這樣的綁定語句或者在頁面的底部運行,或者在 BODY 元素的 onLoad
事件處理器調用的函數中運行。
事件綁定方法V:使用 W3C DOM 的 addEventListener() 方法
Safari 使用的是 W3C DOM 級別2定義的事件綁定機制,這個機制和 IE5/Windows 的 attachEvent()
方法很類似,但是有自己的語法。W3C DOM 規范為 DOM 層次中的每一個結點都定義了一個 addEventListener()
方法。HTML 元素是 DOM 結點中的一類,在一對元素標識內部的文本結點也是一個結點,也能夠接收事件。這一點在 NN6 事件處理過程中經常得到體現,在本文的后面部分您將會看到。
addEventListener()
方法的語法如下所示:
nodeReference.addEventListener("eventType", listenerReference, captureFlag);
用 W3C DOM 規范中的行話來說,addEventListener()
方法為指定的結點注冊了一個事件,表示該結點希望處理相應的事件。這個方法的第一個參數是一個聲明事件類型的字符串(不帶"on"前綴),比如 click
,mousedown
,和 keypress
。addEventListener()
方法的第二個參數可以和早些時候描述過的函數引用同樣對待。第三個參數則是一個 Boolean 值,指明該結點是否以DOM中所謂的捕捉模式來偵聽事件。事件的捕捉和派發---綜合起來稱為事件的傳播--最后由另一篇文章來描述。對于一個典型的事件偵聽器來說,第三個參數應該為 false(假)
。
那種綁定方法最好?
如果您足夠幸運,只需要為某一個操作系統上特定版本的瀏覽器創建應用程序,則可以為選定的瀏覽器選擇最現代的綁定方式。但是對于跨瀏覽器的網站作者來說,選擇綁定方法則需要面對實質性的挑戰。
如果您只計劃支持 IE5/Mac,則可以不考慮 attachEvent()
和 addEventListener()
方法,因為 IE5/Mac 對這兩種方法都不支持。這種情況下,比較實際的選擇有兩種,要么綁定標識屬性,要么綁定對象屬性。這時就需要費心思了。
一方面,W3C DOM Level 2 承認基于標識屬性的方法,并將它推薦為 addEventListener()
方法的可接受代替方法。為了和數以百萬計的腳本相兼容,所有支持腳本編程的瀏覽器都支持基于標識屬性的事件綁定方法。一些自動化的頁面制作工具,比如 DreamWeaver,也把事件處理器的屬性嵌入到 HTML 標識中。
但是另一方面,在元素標識文件中嵌入面向腳本的信息,又不能將內容從風格及行為中分離開來,這和當前的流行趨勢相違背。把事件綁定到對象屬性上的方法聽起來方向是對的,但是在 W3C 關于 HTML,XHTML,或者 DOM 的標準中,并沒有對事件屬性提供“官方”的支持。盡管如此,在實際生活中,除了第一代支持腳本編程的瀏覽器之外,其它瀏覽器都支持這種方法。
一個純標準論者會認為上述的兩種方法都有缺點,但是對于講究實際的開發者來說,即使考慮到未來主流瀏覽器的兼容性,這兩種方法都是“安全”的。
事件的信息礦:事件對象
所有這三種事件模型的核心都是一個事件對象--它是一個抽象的實體,其屬性中包含很多對事件處理函數具有潛在價值的信息。從本文早些時候對事件綁定技術的討論中,您可能可以推斷出事件對象對腳本之所以至關重要,原因之一是除了基于標識屬性的綁定方法以外,其它綁定方法都不支持將參數傳遞到事件處理函數中。
事件對象通過提供足夠的“掛鉤”,使事件處理函數可以讀取事件的特征,從而填補了這個縫隙。因此,事件處理函數可以得到接收事件的元素的引用,以及其它一些有用的信息,比如鼠標動作的坐標,鼠標使用的按鍵,鍵盤上被按壓的鍵,以及在事件發生的過程中是否有修飾鍵被按下(比如檢測 Shift-click 事件)。
訪問事件對象
雖然事件對象的精確構成因為本文討論的三種 DOM(NN4,IE4+,以及 W3C/Safari)的不同而有所變化,但是,一個事件處理函數只能通過以下兩種方式之一來訪問事件對象:NN 方式和 IE 方式。W3C/Safari DOM 事件對象公布給腳本的接口方式和 NN4 的事件對象一樣;而 IE4+ 則有自己的方法。
IE4+ 的事件對象更加易于描述,因此我們首先對它進行討論。簡單地說,事件對象是 window
對象的一個屬性。這意味著在所有的實例中只有一個事件對象。舉例來說,在鍵盤上簡單地按壓和松開一個按鍵,會產生三個事件:onKeyDown
,onKeyPress
,和 onKeyUp
(事件的發生順序和這里的列舉順序相同)。如果 onKeyDown
事 件激活的函數花費很長的時間進行處理,則瀏覽器就會把其它兩個事件保持在隊列中,直到 onMouseDown
事件處理完成為止。
而對于 NN4 和 W3C DOM 來說,事件對象看起來就更加抽象一些。除了基于標識屬性風格的綁定方法之外,其它綁定方法都是把事件對象自動傳遞給與事件相綁定的函數。傳遞給函數的是一個單一的參數。開發者需要在函數中定義一個參數變量,來“接收”該參數的值。為了避免和IE中的 window.event
對象互相沖突,請不要把參數命名為 event。舉例來說,把它命名為 evt
就相當好,相應的事件函數的定義大致如下:
function myFunc(evt) {
// script statements here
}
然而,如果您使用的是基于標識屬性的事件綁定技術,就必須顯式地把事件作為一個參數傳遞到您調用的函數。為了完成事件的傳遞,需要把 event
這個關鍵字作為參數進行傳遞:
onClick = "myFunc(event)"
外部傳入的參數是您的事件處理函數和 NN 的事件對象之間的唯一聯系紐帶。如果在主事件處理函數內部調用的其它函數需要該對象或者該對象的屬性值,則您可以把該對象或其屬性值作為參數中繼給這些函數。
如果您想知道 IE 是否把事件的引用保存在 window.event
屬性中,那答案是“是”。使用這個語法交集是相當安全的,因為在 NN 和 IE 這兩個瀏覽器,被傳遞到事件處理函數的事件對象都有您所期望的當前事件的屬性值。
兼容兩種事件對象引用
設想在處理事件時,我們需要在一個事件函數中考察一個或者多個事件屬性。這是一個簡單的技術,可以使事件處理函數和作為參數傳入的事件對象協同工作,或者從 window.event
屬性中讀取信息。而且,這個技術不必處理不同的瀏覽器版本之間的細微差別。
在開始的時候,需要在您的事件處理函數中定義一個參數變量,準備接收可能傳入的事件對象。然后,通過簡單的條件表達式把瀏覽器的事件對象賦值給上述的參數變量:
function myFunc(evt) {
evt = (evt) ? evt : ((window.event) ? window.event : "")
// process event here
}
如果事件對象真的以參數的形式傳進來了,則在函數內部,事件對象就被保留在 evt
這個局部變量中。如果這個參數是 null
,而且瀏覽器的 window
對象包含有一個 event
屬性,則 window.event
對象就會把自己賦值給 evt
變量。
然而,為了完成這個工作,還應該再包含一層或者多層條件控制,以便優雅地適應那些在事件模型中沒有定義事件對象的的早期瀏覽器:
function myFunc(evt) {
evt = (evt) ? evt : ((window.event) ? window.event : "")
if (evt) {
// process event here
}
}
為了把同樣的方式應用到所有事件處理函數的構建中,您可以定義一個函數來兼容兩種事件,即由綁定的標識屬性顯式傳入的事件對象,以及由綁定的事件屬性隱式傳入的事件對象。這樣即使您在開發過程中改變了事件綁定的風格,這個函數也不必改變。
瑞典自助餐式地選擇事件對象
然而,建立一個指向事件對象的引用只是戰斗的一部分。來自不同事件模型的每一個事件對象都擁有自己的一套屬性,以容納事件的細節。下面的表格列出了最常用的屬性,以及這些屬性在上述三種事件對象類型中的名稱。
表格 1. 流行的事件對象屬性
描述 |
NN4 |
IE4+ |
W3C/Safari |
Event target |
target
|
srcElement
|
target
|
Event type |
type
|
type
|
type
|
X coordinate on page |
pageX
|
* |
pageX
|
Y coordinate on page |
pageY
|
* |
pageY
|
Mouse button |
which
|
button
|
button
|
Keyboard key |
which
|
keyCode
|
keyCode
|
標注*的屬性值可以通過對 event.clientX + document.body.scrollTop
或者 event.clientY + document.body.scrollTop
進行求值來得到。
Macintosh 版本的IE5在通常情況下都遵循 IE4+ 的事件對象模型,但是有一個例外,即 IE5/Mac 的事件對象既定義了 srcElement
屬性,也定義了 target
屬性,這兩個屬性都指向接收事件的元素。
需要抽象的最重要的事件對象屬性可能得算指向接收事件的 HTML 元素的引用。NN4 和 W3C 的事件對象采用相同的屬性名(target
),而 IE4+ 的事件對象則使用 srcElement
屬性。這時候,對象檢測技術(而不是費力勞神而又具有危險傾向的瀏覽器版本識別方法)再次拯救了我們。對于那些非文本容器的元素,一個簡單的條件表達式就可以輕松處理腳本語法上的差別:
var elem = (evt.target) ? evt.target : evt.srcElement
從現在開始,您的腳本就可以讀寫任何瀏覽器對象模型公布出來的元素對象屬性了。
W3C DOM結點的事件目標
W3C DOM 的結點架構使得文檔中的每一個結點都可以接收事件。在支持這一架構的瀏覽器中,發生在嵌套文本頂上的事件并不調用分配給文本容器的事件處理器,相應的文本結點才是該事件的目標結點。考慮如下場景:
在事件實例,當鼠標的指針在一個 SPAN 元素包含的文本頂上滾動時,該文本就會被高亮顯示。 事件綁定的過程通過對象屬性在 init()
函數中進行。從表面上看,當用戶在 SPAN 元素頂上滾動鼠標時,onMouseOver
事件動作函數就為該元素指派一個與風格表單規則相關聯的類名(highlight
),該風格規則把文本的顯示風格定義為粗體,黃色背景;而在 onMouseOut
函數中,則把風格恢復為原始的版本(類 normal
)。請注意一個 toggleHighlight()
函數是如何在事件對象的 type
屬性的幫助下,執行兩個動作的(該屬性在所有事件模型對象中的名稱是相同的)。請試一下這個事件實例。
但是如果您把例子裝載到 NN6,則鼠標事件的真正目標就是 SPAN 元素中的文本結點了。本文并不討論事件的傳播機制,但是請相信,W3C DOM 事件模型的缺省行為會使事件沿著結點的包含層次向上傳播(和 IE4+ 中事件通過元素容器向上傳播的機制很類似)。因此,在這個事件實例中。鼠標事件會從其真正的目標向上傳遞到文本結點的容器(也就是 SPAN 元素)。這些事件觸發了 SPAN 元素中相應的事件處理器。
雖然事件處理器屬于 SPAN 元素,事件對象還是保留文本對象的引用,并將它作為事件的原始目標。然而,只有對文本結點的容器進行動作,才能修改它的風格。為了實現 toggleHighlight()
函數的等價操作,使之可以修改SPAN容器的 className
屬性,該函數需要派生出一個指向文本結點容器的引用。
一個策略是使用 W3C DOM 事件對象的 currentTarget
屬性,該屬性返回一個處理事件的結點的引用。腳本中的決策樹需要考慮這個屬性,增加代碼之后的 toggleHighlight()
函數如下所示:
function toggleHighlight(evt) {
evt = (evt) ? evt : ((window.event) ? window.event : "")
if (evt) {
var elem
if (evt.target) {
if (evt.currentTarget && (evt.currentTarget != evt.target)) {
elem = evt.currentTarget
} else {
elem = evt.target
}
} else {
elem = evt.srcElement
}
elem.className = (evt.type == "mouseover") ? "highlight" : "normal"
}
}
另一個可選的方法是考察由 target
屬性返回的對象的 ronodeType
屬性。一個能夠把事件定向給文本結點的瀏覽器,也可以把一個文本結點的 nodeType
屬性值報告為3,而不是報告為元素結點的類型(其值為1)。如果事件的目標是一個文本結點,則腳本程序就可以通過該文本結點的 parentNode
屬性來得到其上級元素結點的引用。這種方法的決策樹在某種程度上得到更多的改進:
function toggleHighlight(evt) {
evt = (evt) ? evt : ((window.event) ? window.event : "")
if (evt) {
var elem
if (evt.target) {
elem = (evt.target.nodeType == 3) ? evt.target.parentNode : evt.target
} else {
elem = evt.srcElement
}
elem.className = (evt.type == "mouseover") ? "highlight" : "normal"
}
}
如果您正在用遵循 W3 的瀏覽器閱讀本文,則請嘗試這個修改過的版本,看看鼠標滾動時的風格變化。
這個頁面使用了嵌入到事件實例中的最新版本的 toggleHighlight()
函數,展示了如何使用 JavaScript 為那些能夠顯示期望效果的瀏覽器增加額外的價值,同時也可以那些基本的內容提供給仍然使用著較老版本或者不支持腳本編程的瀏覽器的用戶,只不過在模式上不那么動人和便于交互。
一個事件處理函數的模板
并不是每個事件處理函數都處理頁面元素對象中同樣的屬性或者行為,但是,從上文的討論可以派生出來的一個模板,您可以在這個模板的幫助下開始編碼。模板如下:
function functionName(evt) {
evt = (evt) ? evt : ((window.event) ? window.event : "")
if (evt) {
var elem
if (evt.target) {
elem = (evt.target.nodeType == 3) ? evt.target.parentNode : evt.target
} else {
elem = evt.srcElement
}
if (elem) {
// process event here
}
}
}
請把第一行的函數名替換為您希望的函數名,并在注視指示的地方開始書寫具體事件的代碼。這個格式應該可以為您提供一個起點,適合于您采用的任何跨瀏覽器的事件綁定風格。如果您需要在一個頁面中多次使用這個格式,則可以進一步精簡代碼,即把讀取目標的代碼抽象成一個可重用的工具函數,然后在每一個事件處理函數中進行調用:
// shared function
function getTargetElement(evt) {
var elem
if (evt.target) {
elem = (evt.target.nodeType == 3) ? evt.target.parentNode : evt.target
} else {
elem = evt.srcElement
}
return elem
}
function functionName(evt) {
evt = (evt) ? evt : ((window.event) ? window.event : "")
if (evt) {
var elem = getTargetElement(evt)
if (elem) {
// process event here
}
}
}
有了這類框架,您現在應該可以把更多的注意力集中在各個事件處理函數要求的具體動作中了。
查看實例:
下載腳本