JavaScript 程式採用了非同步事件驅動程式設計模型。在這種程式設計風格下,當文件、瀏覽器、元素或與之相關的物件發生某些有趣的事情時,Web 瀏覽器就會產生事件(event)。例如,當 Web 瀏覽器載入完文件、使用者把滑鼠指標移到超連結上或敲擊鍵盤時,Web 瀏覽器都會產生事件。如果 JavaScript 應用程式關注特定型別的事件,那麼它可以註冊當這類事件發生時要呼叫的一個或多個函式。請注意,這種風格並不只應用於 Web 程式設計,所有使用圖形使用者介面的應用程式都採用了它,它們靜待某些事情發生(即,它們等待事件發生),然後它們響應。
請注意,事件本身並不是一個需要定義的技術名詞。簡而言之,事件就是 Web 瀏覽器通知應用程式發生了什麼事情,這種在傳統軟體工程中被稱為觀察員模式。
事件流
當瀏覽器發展到第四代時(IE4 及 Netscape Communicator 4),瀏覽器開發團隊遇到了一個很有意思的問題:頁面的哪一部分會擁有某個特定的事件?要明白這個問題問的是什麼,可以想象畫在一張紙上的一組同心圓。如果你把手指放在圓心上,那麼你的手指指向的不是一個圓,而是紙上的所有圓。兩家公司的瀏覽器開發團隊在看待瀏覽器事件方面還是一致的。如果你單擊了某個按鈕,他們都認為單擊事件不僅僅發生在按鈕上。換句話說,在單擊按鈕的同時,你也單擊了按鈕的容器元素,甚至也單擊了整個頁面。
事件流描述的是從頁面中接收事件的順序。但有意思的是,IE 和 Netscape 開發團隊居然提出了差不多是完全相反的事件流的概念。IE 的事件流是事件冒泡流,而 Netscape Communicator 的事件流是事件捕獲流。
事件冒泡
IE 的事件流叫做事件冒泡(event bubbling),即事件開始時由最具體的元素(文件中巢狀層次最深的那個節點)接收,然後逐級向上傳播到較為不具體的節點(文件)。以下面的HTML頁面為例:
<!DOCTYPE html>
<html>
<head>
<title>Event Bubbling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>複製程式碼
如果你單擊了頁面中的 <div>
元素,那麼這個 click
事件會按照如下順序傳播:
<div>
<body>
<html>
document
也就是說,click
事件首先在 <div>
元素上發生,而這個元素就是我們單擊的元素。然後,click
事件沿 DOM 樹向上傳播,在每一級節點上都會發生,直至傳播到 document
物件。下圖展示了事件冒泡的過程。
事件捕獲
Netscape Communicator 團隊提出的另一種事件流叫做事件捕獲(event capturing)。事件捕獲的思想是不太具體的節點應該更早接收到事件,而最具體的節點應該最後接收到事件。事件捕獲的用意在於在事件到達預定目標之前捕獲它。如果仍以前面的 HTML 頁面作為演示事件捕獲的例子,那麼單擊 <div>
元素就會以下列順序觸發 click
事件。
document
<html>
<body>
<div>
在事件捕獲過程中,document
物件首先接收到 click
事件,然後事件沿 DOM 樹依次向下,一直傳播到事件的實際目標,即 <div>
元素。下圖展示了事件捕獲的過程。
由於老版本的瀏覽器不支援,因此很少有人使用事件捕獲。我們也建議大家放心地使用事件冒泡,在有特殊需要時再使用事件捕獲。
事件處理程式
事件就是使用者或瀏覽器自身執行的某種動作。諸如 click
、load
和 mouseover
,都是事件的名字。而響應某個事件的函式就叫做事件處理程式(或事件偵聽器)。事件處理程式的名字以 "on"
開頭,因此 click
事件的事件處理程式就是 onclick
,load
事件的事件處理程式就是 onload
。為事件指定處理程式的方式有好幾種。
HTML 事件處理程式
某個元素支援的每種事件,都可以使用一個與相應事件處理程式同名的 HTML 特性來指定。這個特性的值應該是能夠執行的 JavaScript 程式碼。例如,要在按鈕被單擊時執行一些 JavaScript,可以像下面這樣編寫程式碼:
<input type="button" value="Click Me" onclick="console.log('Clicked')" />複製程式碼
當單擊這個按鈕時,就會在控制檯列印 "Clicked"
。這個操作是通過指定 onclick
特性並將一些 JavaScript 程式碼作為它的值來定義的。由於這個值是 JavaScript,因此不能在其中使用未經轉義的 HTML 語法字元,例如和號(&)、雙引號("")、小於號(<)或大於號(>)。為了避免使用 HTML 實體,這裡使用了單引號。如果想要使用雙引號,那麼就要將程式碼改寫成如下所示:
<input type="button" value="Click Me" onclick="console.log("Clicked")" />複製程式碼
在 HTML 中定義的事件處理程式可以包含要執行的具體動作,也可以呼叫在頁面其他地方定義的指令碼,如下面的例子所示:
<script type="text/javascript">
function showMessage(){
console.log("Hello world!");
}
</script>
<input type="button" value="Click Me" onclick="showMessage()" />複製程式碼
在這個例子中,單擊按鈕就會呼叫 showMessage()
函式。這個函式是在一個獨立的 <script>
元素中定義的,當然也可以被包含在一個外部檔案中。事件處理程式中的程式碼在執行時,有權訪問全域性作用域中的任何程式碼。
這樣指定事件處理程式具有一些獨到之處。首先,這樣會建立一個封裝著元素屬性值的函式。這個函式中有一個區域性變數 event
,也就是事件物件:
<!-- 輸出 "click" -->
<input type="button" value="Click Me" onclick="console.log(event.type)">複製程式碼
通過 event
變數,可以直接訪問事件物件,你不用自己定義它,也不用從函式的引數列表中讀取。
在這個函式內部,this
值等於事件的目標元素,例如:
<!-- 輸出 "Click Me" -->
<input type="button" value="Click Me" onclick="console.log(this.value)">複製程式碼
如此一來,事件處理程式要訪問自己的屬性就簡單多了。下面這行程式碼與前面的例子效果相同:
<!-- 輸出 "Click Me" -->
<input type="button" value="Click Me" onclick="console.log(value)">複製程式碼
不過,在 HTML 中指定事件處理程式有三個缺點。首先,存在一個時差問題。因為使用者可能會在 HTML 元素一出現在頁面上就觸發相應的事件,但當時的事件處理程式有可能尚不具備執行條件。以前面的例子來說明,假設 showMessage()
函式是在按鈕下方、頁面的最底部定義的。如果使用者在頁面解析 showMessage()
函式之前就單擊了按鈕,就會引發錯誤。為此,很多HTML事件處理程式都會被封裝在一個 try-catch
塊中,以便錯誤不會浮出水面,如下面的例子所示:
<input type="button" value="Click Me" onclick="try{showMessage();}catch(ex){}">複製程式碼
這樣,如果在 showMessage()
函式有定義之前單擊了按鈕,使用者將不會看到 JavaScript 錯誤,因為在瀏覽器有機會處理錯誤之前,錯誤就被捕獲了。
第二個缺點是,這樣擴充套件事件處理程式的作用域鏈在不同瀏覽器中會導致不同結果。不同 JavaScript 引擎遵循的識別符號解析規則略有差異,很可能會在訪問非限定物件成員時出錯。
第三個缺點是,HTML 與 JavaScript 程式碼緊密耦合。如果要更換事件處理程式,就要改動兩個地方:HTML 程式碼和 JavaScript 程式碼。而這正是許多開發人員摒棄 HTML 事件處理程式,轉而使用 JavaScript 指定事件處理程式的原因所在。
DOM1 級事件處理程式
通過 JavaScript 指定事件處理程式的傳統方式,就是將一個函式賦值給一個事件處理程式屬性。這種為事件處理程式賦值的方法是在第四代Web瀏覽器中出現的,而且至今仍然為所有現代瀏覽器所支援。原因一是簡單,二是具有跨瀏覽器的優勢。要使用 JavaScript 指定事件處理程式,首先必須取得一個要操作的物件的引用。
每個元素(包括 window
和 document
)都有自己的事件處理程式屬性,這些屬性通常全部小寫,例如 onclick
。將這種屬性的值設定為一個函式,就可以指定事件處理程式,如下所示:
var btn = document.getElementById("myBtn");
btn.onclick = function(){
console.log("Clicked");
};複製程式碼
在此,我們通過文件物件取得了一個按鈕的引用,然後為它指定了 onclick
事件處理程式。但要注意,在這些程式碼執行以前不會指定事件處理程式,因此如果這些程式碼在頁面中位於按鈕後面,就有可能在一段時間內怎麼單擊都沒有反應。
使用 DOM1 級方法指定的事件處理程式被認為是元素的方法。因此,這時候的事件處理程式是在元素的作用域中執行;換句話說,程式中的 this
引用當前元素。來看一個例子。
var btn = document.getElementById("myBtn");
btn.onclick = function(){
console.log(this.id); // "myBtn"
};複製程式碼
單擊按鈕顯示的是元素的 ID,這個 ID 是通過 this.id
取得的。不僅僅是 ID,實際上可以在事件處理程式中通過 this
訪問元素的任何屬性和方法。以這種方式新增的事件處理程式會在事件流的冒泡階段被處理。
也可以刪除通過 DOM1 級方法指定的事件處理程式,只要像下面這樣將事件處理程式屬性的值設定為 null
即可:
btn.onclick = null; // 刪除事件處理程式複製程式碼
將事件處理程式設定為 null
之後,再單擊按鈕將不會有任何動作發生。
DOM2 級事件處理程式
DOM2 級事件定義了兩個方法,用於處理指定和刪除事件處理程式的操作:addEventListener()
和 removeEventListener()
。所有 DOM 節點中都包含這兩個方法,並且它們都接受3個引數:要處理的事件名、作為事件處理程式的函式和一個布林值。最後這個布林值引數如果是 true
,表示在捕獲階段呼叫事件處理程式;如果是 false
,表示在冒泡階段呼叫事件處理程式。
要在按鈕上為 click
事件新增事件處理程式,可以使用下列程式碼:
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
console.log(this.id);
}, false);複製程式碼
上面的程式碼為一個按鈕新增了 onclick
事件處理程式,而且該事件會在冒泡階段被觸發(因為最後一個引數是 false
)。與 DOM1 級方法一樣,這裡新增的事件處理程式也是在其依附的元素的作用域中執行。使用 DOM2 級方法新增事件處理程式的主要好處是可以新增多個事件處理程式。來看下面的例子。
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
console.log(this.id);
}, false);
btn.addEventListener("click", function(){
console.log("Hello world!");
}, false);複製程式碼
這裡為按鈕新增了兩個事件處理程式。這兩個事件處理程式會按照新增它們的順序觸發,因此首先會顯示元素的 ID,其次會顯示 "Hello world!"
訊息。
通過 addEventListener()
新增的事件處理程式只能使用 removeEventListener()
來移除;移除時傳入的引數與新增處理程式時使用的引數相同。這也意味著通過 addEventListener()
新增的匿名函式將無法移除,如下面的例子所示。
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
console.log(this.id);
}, false);
btn.removeEventListener("click", function(){ // 沒有用!
console.log(this.id);
}, false);複製程式碼
在這個例子中,我們使用 addEventListener()
新增了一個事件處理程式。雖然呼叫 removeEventListener()
時看似使用了相同的引數,但實際上,第二個引數與傳入 addEventListener()
中的那一個是完全不同的函式。而傳入 removeEventListener()
中的事件處理程式函式必須與傳 入addEventListener()
中的相同,如下面的例子所示。
var btn = document.getElementById("myBtn");
var handler = function(){
console.log(this.id);
};
btn.addEventListener("click", handler, false);
btn.removeEventListener("click", handler, false); // 有效!複製程式碼
重寫後的這個例子沒有問題,是因為在 addEventListener()
和 removeEventListener()
中使用了相同的函式。
大多數情況下,都是將事件處理程式新增到事件流的冒泡階段,這樣可以最大限度地相容各種瀏覽器。最好只在需要在事件到達目標之前截獲它的時候將事件處理程式新增到捕獲階段。如果不是特別需要,我們不建議在事件捕獲階段註冊事件處理程式。
IE9、Firefox、Safari、Chrome 和 Opera 支援 DOM2 級事件處理程式。
IE 事件處理程式
IE 實現了與 DOM 中類似的兩個方法:attachEvent()
和 detachEvent()
。這兩個方法接受相同的兩個引數:事件處理程式名稱與事件處理程式函式。由於 IE8 及更早版本只支援事件冒泡,所以通過 attachEvent()
新增的事件處理程式都會被新增到冒泡階段。
要使用 attachEvent()
為按鈕新增一個事件處理程式,可以使用以下程式碼。
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
console.log("Clicked");
});複製程式碼
注意,attachEvent()
的第一個引數是 "onclick"
,而非 DOM 的 addEventListener()
方法中的 "click"
。
在 IE 中使用 attachEvent()
與使用 DOM1 級方法的主要區別在於事件處理程式的作用域。在使用 DOM1 級方法的情況下,事件處理程式會在其所屬元素的作用域內執行;在使用 attachEvent()
方法的情況下,事件處理程式會在全域性作用域中執行,因此 this
等於 window
。來看下面的例子。
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
console.log(this === window); // true
});複製程式碼
在編寫跨瀏覽器的程式碼時,牢記這一區別非常重要。
與 addEventListener()
類似,attachEvent()
方法也可以用來為一個元素新增多個事件處理程式。不過,與 DOM 方法不同的是,這些事件處理程式不是以新增它們的順序執行,而是以相反的順序被觸發。
使用 attachEvent()
新增的事件可以通過 detachEvent()
來移除,條件是必須提供相同的引數。與 DOM 方法一樣,這也意味著新增的匿名函式將不能被移除。不過,只要能夠將對相同函式的引用傳給 detachEvent()
,就可以移除相應的事件處理程式。
支援 IE 事件處理程式的瀏覽器有 IE 和 Opera。
跨瀏覽器的事件處理程式
為了以跨瀏覽器的方式處理事件,不少開發人員會使用能夠隔離瀏覽器差異的 JavaScript 庫,還有一些開發人員會自己開發最合適的事件處理的方法。自己編寫程式碼其實也不難,只要恰當地使用能力檢測即可。要保證處理事件的程式碼能在大多數瀏覽器下一致地執行,只需關注冒泡階段。
第一個要建立的方法是 addHandler()
,它的職責是視情況分別使用 DOM1 級方法、DOM2 級方法或 IE 方法來新增事件。這個方法屬於一個名叫 EventUtil
的物件,本書將使用這個物件來處理瀏覽器間的差異。addHandler()
方法接受3個引數:要操作的元素、事件名稱和事件處理程式函式。
與 addHandler()
對應的方法是 removeHandler()
,它也接受相同的引數。這個方法的職責是移除之前新增的事件處理程式——無論該事件處理程式是採取什麼方式新增到元素中的,如果其他方法無效,預設採用 DOM1 級方法。
EventUtil
的用法如下所示。
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;
}
}
};複製程式碼
這兩個方法首先都會檢測傳入的元素中是否存在 DOM2 級方法。如果存在 DOM2 級方法,則使用該方法:傳入事件型別、事件處理程式函式和第三個引數 false
(表示冒泡階段)。如果存在的是 IE 的方法,則採取第二種方案。注意,為了在 IE8 及更早版本中執行,此時的事件型別必須加上 "on"
字首。最後一種可能就是使用 DOM1 級方法(在現代瀏覽器中,應該不會執行這裡的程式碼)。此時,我們使用的是方括號語法來將屬性名指定為事件處理程式,或者將屬性設定為 null
。
可以像下面這樣使用 EventUtil
物件:
var btn = document.getElementById("myBtn");
var handler = function(){
console.log("Clicked");
};
EventUtil.addHandler(btn, "click", handler);
EventUtil.removeHandler(btn, "click", handler);複製程式碼
addHandler()
和 removeHandler()
沒有考慮到所有的瀏覽器問題,例如在 IE 中的作用域問題。不過,使用它們新增和移除事件處理程式還是足夠了。
事件物件
在觸發 DOM 上的某個事件時,會產生一個事件物件 event
,這個物件中包含著所有與事件有關的資訊。包括導致事件的元素、事件的型別以及其他與特定事件相關的資訊。例如,滑鼠操作導致的事件物件中,會包含滑鼠位置的資訊,而鍵盤操作導致的事件物件中,會包含與按下的鍵有關的資訊。所有瀏覽器都支援 event
物件,但支援方式不同。
DOM 中的事件物件
相容 DOM 的瀏覽器會將一個 event
物件傳入到事件處理程式中。無論指定事件處理程式時使用什麼方法(DOM1 級或 DOM2 級),都會傳入 event
物件。來看下面的例子。
var btn = document.getElementById("myBtn");
btn.onclick = function(event){
console.log(event.type); // "click"
};
btn.addEventListener("click", function(event){
console.log(event.type); // "click"
}, false);複製程式碼
這個例子中的兩個事件處理程式都會彈出一個警告框,顯示由 event.type
屬性表示的事件型別。這個屬性始終都會包含被觸發的事件型別,例如 "click"
(與傳入 addEventListener()
和 removeEventListener()
中的事件型別一致)。
在通過 HTML 特性指定事件處理程式時,變數 event
中儲存著 event
物件。請看下面的例子。
<input type="button" value="Click Me" onclick="console.log(event.type)"/>複製程式碼
以這種方式提供 event
物件,可以讓 HTML 特性事件處理程式與 JavaScript 函式執行相同的操作。
event
物件包含與建立它的特定事件有關的屬性和方法。觸發的事件型別不一樣,可用的屬性和方法也不一樣。不過,所有事件都會有下表列出的成員。
bubbles
,表明事件是否冒泡。cancelable
,表明是否可以取消事件的預設行為。currentTarget
,其事件處理程式當前正在處理事件的那個元素。defaultPrevented
,為true
表示已經呼叫了preventDefault()
(DOM3 級事件中新增)。detail
,與事件相關的細節資訊。eventPhase
,呼叫事件處理程式的階段:1表示捕獲階段,2表示“處於目標”,3表示冒泡階段。preventDefault()
,取消事件的預設行為。如果cancelable
是true
,則可以使用這個方法。stopImmediatePropagation()
,取消事件的進一步捕獲或冒泡,同時阻止任何事件處理程式被呼叫(DOM3 級事件中新增)。stopPropagation()
,取消事件的進一步捕獲或冒泡。如果bubbles
為true
,則可以使用這個方法。target
,事件的目標。trusted
,為true
表示事件是瀏覽器生成的。為false
表示事件是由開發人員通過 JavaScript 建立的(DOM3 級事件中新增)。type
,被觸發的事件的型別。view
,與事件關聯的抽象檢視,等同於發生事件的window
物件。
在事件處理程式內部,物件 this
始終等於 currentTarget
的值,而 target
則只包含事件的實際目標。如果直接將事件處理程式指定給了目標元素,則 this
、currentTarget
和 target
包含相同的值。來看下面的例子。
var btn = document.getElementById("myBtn");
btn.onclick = function(event){
console.log(event.currentTarget === this); // true
console.log(event.target === this); // true
};複製程式碼
這個例子檢測了 currentTarget
和 target
與 this
的值。由於 click
事件的目標是按鈕,因此這三個值是相等的。如果事件處理程式存在於按鈕的父節點中(例如 document.body
),那麼這些值是不相同的。再看下面的例子。
document.body.onclick = function(event){
console.log(event.currentTarget === document.body); // true
console.log(this === document.body); // true
console.log(event.target === document.getElementById("myBtn")); // true
};複製程式碼
當單擊這個例子中的按鈕時,this
和 currentTarget
都等於document.body
,因為事件處理程式是註冊到這個元素上的。然而,target
元素卻等於按鈕元素,因為它是 click
事件真正的目標。由於按鈕上並沒有註冊事件處理程式,結果 click
事件就冒泡到了 document.body
,在那裡事件才得到了處理。
在需要通過一個函式處理多個事件時,可以使用 type
屬性。例如:
var btn = document.getElementById("myBtn");
var handler = function(event){
switch(event.type){
case "click":
console.log("Clicked");
break;
case "mouseover":
event.target.style.backgroundColor = "red";
break;
case "mouseout":
event.target.style.backgroundColor = "";
break;
}
};
btn.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;複製程式碼
這個例子定義了一個名為 handler
的函式,用於處理3種事件:click
、mouseover
和 mouseout
。當單擊按鈕時,會出現一個與前面例子中一樣的警告框。當按鈕移動到按鈕上面時,背景顏色應該會變成紅色,而當滑鼠移動出按鈕的範圍時,背景顏色應該會恢復為預設值。這裡通過檢測 event.type
屬性,讓函式能夠確定發生了什麼事件,並執行相應的操作。
要阻止特定事件的預設行為,可以使用 preventDefault()
方法。例如,連結的預設行為就是在被單擊時會導航到其 href
特性指定的 URL。如果你想阻止連結導航這一預設行為,那麼通過連結的 onclick
事件處理程式可以取消它,如下面的例子所示。
var link = document.getElementById("myLink");
link.onclick = function(event){
event.preventDefault();
};複製程式碼
只有 cancelable
屬性設定為 true
的事件,才可以使用 preventDefault()
來取消其預設行為。
另外,stopPropagation()
方法用於立即停止事件在 DOM 層次中的傳播,即取消進一步的事件捕獲或冒泡。例如,直接新增到一個按鈕的事件處理程式可以呼叫 stopPropagation()
,從而避免觸發註冊在 document.body
上面的事件處理程式,如下面的例子所示。
var btn = document.getElementById("myBtn");
btn.onclick = function(event){
console.log("Clicked");
event.stopPropagation();
};
document.body.onclick = function(event){
console.log("Body clicked");
};複製程式碼
對於這個例子而言,如果不呼叫 stopPropagation()
,就會在單擊按鈕時出現兩個警告框。可是,由於 click
事件根本不會傳播到 document.body
,因此就不會觸發註冊在這個元素上的 onclick
事件處理程式。
事件物件的 eventPhase
屬性,可以用來確定事件當前正位於事件流的哪個階段。如果是在捕獲階段呼叫的事件處理程式,那麼 eventPhase
等於 1
;如果事件處理程式處於目標物件上,則 eventPhase
等於 2
;如果是在冒泡階段呼叫的事件處理程式,eventPhase
等於 3
。這裡要注意的是,儘管“處於目標”發生在冒泡階段,但 eventPhase
仍然一直等於 2
。來看下面的例子。
var btn = document.getElementById("myBtn");
btn.onclick = function(event){
console.log(event.eventPhase); // 2
};
document.body.addEventListener("click", function(event){
console.log(event.eventPhase); // 1
}, true);
document.body.onclick = function(event){
console.log(event.eventPhase); // 3
};複製程式碼
當單擊這個例子中的按鈕時,首先執行的事件處理程式是在捕獲階段觸發的新增到 document.body
中的那一個,結果會彈出一個警告框顯示錶示 eventPhase
的 1
。接著,會觸發在按鈕上註冊的事件處理程式,此時的 eventPhase
值為 2
。最後一個被觸發的事件處理程式,是在冒泡階段執行的新增到 document.body
上的那一個,顯示 eventPhase
的值為 3
。而當 eventPhase
等於 2
時,this
、target
和 currentTarget
始終都是相等的。
只有在事件處理程式執行期間,event物件才會存在;一旦事件處理程式執行完成,event物件就會被銷燬。
IE 中的事件物件
與訪問 DOM 中的 event
物件不同,要訪問IE中的 event
物件有幾種不同的方式,取決於指定事件處理程式的方法。在使用 DOM1 級方法新增事件處理程式時,event
物件作為 window
物件的一個屬性存在。來看下面的例子。
var btn = document.getElementById("myBtn");
btn.onclick = function(){
var event = window.event;
console.log(event.type); // "click"
};複製程式碼
在此,我們通過 window.event
取得了 event
物件,並檢測了被觸發事件的型別(IE 中的 type
屬性與 DOM 中的 type
屬性是相同的)。可是,如果事件處理程式是使用 attachEvent()
新增的,那麼就會有一個 event
物件作為引數被傳入事件處理程式函式中,如下所示。
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(event){
console.log(event.type); // "click"
});複製程式碼
在像這樣使用 attachEvent()
的情況下,也可以通過 window
物件來訪問 event
物件,就像使用 DOM1 級方法時一樣。不過為方便起見,同一個物件也會作為引數傳遞。
如果是通過 HTML 特性指定的事件處理程式,那麼還可以通過一個名叫 event
的變數來訪問 event
物件(與 DOM 中的事件模型相同)。再看一個例子。
<input type="button" value="Click Me" onclick="console.log(event.type)">複製程式碼
IE 的 event
物件同樣也包含與建立它的事件相關的屬性和方法。其中很多屬性和方法都有對應的或者相關的 DOM 屬性和方法。與 DOM 的 event
物件一樣,這些屬性和方法也會因為事件型別的不同而不同,但所有事件物件都會包含下表所列的屬性和方法。
cancelBubble
,預設值為false
,但將其設定為true
就可以取消事件冒泡(與 DOM 中的stopPropagation()
方法的作用相同)。returnValue
,預設值為true
,但將其設定為false
就可以取消事件的預設行為(與 DOM 中的preventDefault()
方法的作用相同) 。srcElement
,事件的目標(與 DOM 中的target
屬性相同) 。type
,被觸發的事件的型別 。
因為事件處理程式的作用域是根據指定它的方式來確定的,所以不能認為 this
會始終等於事件目標。故而,最好還是使用 event.srcElement
比較保險。例如:
var btn = document.getElementById("myBtn");
btn.onclick = function(){
console.log(window.event.srcElement === this); // true
};
btn.attachEvent("onclick", function(event){
console.log(event.srcElement === this); // false
});複製程式碼
在第一個事件處理程式中(使用 DOM1 級方法指定的),srcElement
屬性等於 this
,但在第二個事件處理程式中,這兩者的值不相同。
如前所述,returnValue
屬性相當於 DOM 中的 preventDefault()
方法,它們的作用都是取消給定事件的預設行為。只要將 returnValue
設定為 false
,就可以阻止預設行為。來看下面的例子。
var link = document.getElementById("myLink");
link.onclick = function(){
window.event.returnValue = false;
};複製程式碼
這個例子在 onclick
事件處理程式中使用 returnValue
達到了阻止連結預設行為的目的。與 DOM 不同的是,在此沒有辦法確定事件是否能被取消。
相應地,cancelBubble
屬性與 DOM 中的 stopPropagation()
方法作用相同,都是用來停止事件冒泡的。由於IE不支援事件捕獲,因而只能取消事件冒泡;但 stopPropagatioin()
可以同時取消事件捕獲和冒泡。例如:
var btn = document.getElementById("myBtn");
btn.onclick = function(){
console.log("Clicked");
window.event.cancelBubble = true;
};
document.body.onclick = function(){
console.log("Body clicked");
};複製程式碼
通過在 onclick
事件處理程式中將 cancelBubble
設定為 true
,就可阻止事件通過冒泡而觸發 document.body
中註冊的事件處理程式。結果,在單擊按鈕之後,只會顯示一個警告框。
跨瀏覽器的事件物件
雖然 DOM 和 IE 中的 event
物件不同,但基於它們之間的相似性依舊可以拿出跨瀏覽器的方案來。IE中 event
物件的全部資訊和方法 DOM 物件中都有,只不過實現方式不一樣。不過,這種對應關係讓實現兩種事件模型之間的對映非常容易。可以對前面介紹的 EventUtil
物件加以增強,新增如下方法以求同存異。
var EventUtil = {
addHandler: function(element, type, handler){
// 省略的程式碼
},
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;
}
},
removeHandler: function(element, type, handler){
// 省略的程式碼
},
stopPropagation: function(event){
if (event.stopPropagation){
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
};複製程式碼
以上程式碼顯示,我們為 EventUtil
新增了4個新方法。第一個是 getEvent()
,它返回對 event
物件的引用。考慮到 IE 中事件物件的位置不同,可以使用這個方法來取得 event
物件,而不必擔心指定事件處理程式的方式。在使用這個方法時,必須假設有一個事件物件傳入到事件處理程式中,而且要把該變數傳給這個方法,如下所示。
btn.onclick = function(event){
event = EventUtil.getEvent(event);
};複製程式碼
在相容 DOM 的瀏覽器中,event
變數只是簡單地傳入和返回。而在 IE 中,event
引數是未定義的 undefined
,因此就會返回 window.event
。將這一行程式碼新增到事件處理程式的開頭,就可以確保隨時都能使用 event
物件,而不必擔心使用者使用的是什麼瀏覽器。
第二個方法是 getTarget()
,它返回事件的目標。在這個方法內部,會檢測 event
物件的 target
屬性,如果存在則返回該屬性的值;否則,返回 srcElement
屬性的值。可以像下面這樣使用這個方法。
btn.onclick = function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
};複製程式碼
第三個方法是 preventDefault()
,用於取消事件的預設行為。在傳入 event
物件後,這個方法會檢查是否存在 preventDefault()
方法,如果存在則呼叫該方法。如果 preventDefault()
方法不存在,則將 returnValue
設定為 false
。下面是使用這個方法的例子。
var link = document.getElementById("myLink");
link.onclick = function(event){
event = EventUtil.getEvent(event);
EventUtil.preventDefault(event);
};複製程式碼
以上程式碼可以確保在所有瀏覽器中單擊該連結都不會開啟另一個頁面。首先,使用 EventUtil.getEvent()
取得 event
物件,然後將其傳入到 EventUtil.preventDefault()
以取消預設行為。
第四個方法是 stopPropagation()
,其實現方式類似。首先嚐試使用DOM方法阻止事件流,否則就使用 cancelBubble
屬性。下面看一個例子。
var btn = document.getElementById("myBtn");
btn.onclick = function(event){
console.log("Clicked");
event = EventUtil.getEvent(event);
EventUtil.stopPropagation(event);
};
document.body.onclick = function(event){
console.log("Body clicked");
};複製程式碼
在此,首先使用 EventUtil.getEvent()
取得了 event
物件,然後又將其傳入到 EventUtil.stopPropagation()
。別忘了由於 IE 不支援事件捕獲,因此這個方法在跨瀏覽器的情況下,也只能用來阻止事件冒泡。
事件型別
Web 瀏覽器中可能發生的事件有很多型別。如前所述,不同的事件型別具有不同的資訊,而 DOM3 級事件規定了以下幾類事件。
- UI(User Interface,使用者介面)事件,當使用者與頁面上的元素互動時觸發;
- 焦點事件,當元素獲得或失去焦點時觸發;
- 滑鼠事件,當使用者通過滑鼠在頁面上執行操作時觸發;
- 滾輪事件,當使用滑鼠滾輪(或類似裝置)時觸發;
- 文字事件,當在文件中輸入文字時觸發;
- 鍵盤事件,當使用者通過鍵盤在頁面上執行操作時觸發;
- 合成事件,當為IME(Input Method Editor,輸入法編輯器)輸入字元時觸發;
- 變動(mutation)事件,當底層 DOM 結構發生變化時觸發。
- 變動名稱事件,當元素或屬性名變動時觸發。此類事件已經被廢棄,沒有任何瀏覽器實現它們,因此本章不做介紹。
除了這幾類事件之外,HTML5 也定義了一組事件,而有些瀏覽器還會在 DOM 和 BOM 中實現其他專有事件。這些專有的事件一般都是根據開發人員需求定製的,沒有什麼規範,因此不同瀏覽器的實現有可能不一致。
DOM3 級事件模組在 DOM2 級事件模組基礎上重新定義了這些事件,也新增了一些新事件。包括 IE9 在內的所有主流瀏覽器都支援 DOM2 級事件。 IE9 也支援 DOM3 級事件。
想要了解更多 DOM 和 HTML5 事件,請參見最新版的 W3C 規範:
www.w3.org/TR/uievents…
小結
事件是將 JavaScript 與網頁聯絡在一起的主要方式。DOM3 級事件規範和 HTML5 定義了常見的大多數事件。即使有規範定義了基本事件,但很多瀏覽器仍然在規範之外實現了自己的專有事件,從而為開發人員提供更多掌握使用者互動的手段。有些專有事件與特定裝置關聯,例如移動 Safari 中的 orientationchange
事件就是特定關聯 iOS 裝置的。
在使用事件時,需要考慮如下一些記憶體與效能方面的問題。
- 有必要限制一個頁面中事件處理程式的數量,數量太多會導致佔用大量記憶體,而且也會讓使用者感覺頁面反應不夠靈敏。
- 建立在事件冒泡機制之上的事件委託技術,可以有效地減少事件處理程式的數量。
- 建議在瀏覽器解除安裝頁面之前移除頁面中的所有事件處理程式。
可以使用 JavaScript 在瀏覽器中模擬事件。DOM2 級事件和 DOM3 級事件規範規定了模擬事件的方法,為模擬各種有定義的事件提供了方便。此外,通過組合使用一些技術,還可以在某種程度上模擬鍵盤事件。IE8 及之前版本同樣支援事件模擬,只不過模擬的過程有些差異。
關卡
憑理解和記憶手寫 EventUtil
通用類。
var EventUtil = {
addHandler: function(element, type, handler){
// 待補充的程式碼
},
removeHandler: function(element, type, handler){
// 待補充的程式碼
},
getEvent: function(event){
// 待補充的程式碼
},
getTarget: function(event){
// 待補充的程式碼
},
preventDefault: function(event){
// 待補充的程式碼
},
stopPropagation: function(event){
// 待補充的程式碼
}
};複製程式碼
更多
關注微信公眾號「劼哥舍」回覆「答案」,獲取關卡詳解。
關注 github.com/stone0090/j…,獲取最新動態。