一、介紹
事件是用來實現js和html之間交互的,可以用偵聽器(或處理程序)來預訂事件,以便事件發生時執行相應的代碼。這種在傳統軟件工程中被稱爲觀察員模式的模型,支持頁面的行爲(js)與頁面的外觀(html和css)的鬆散耦合。事件最早是在IE3和Netscape Navigator2中出現的,當時是作爲分擔服務器運算負載的一種手段。
二、事件流
事件流描述的是從頁面中接收事件的順序。IE的事件流是事件冒泡流,而Netscape Communicator 的事件流是事件捕獲流。
1、事件冒泡
IE 的事件流叫做事件冒泡,即事件開始時由最具體的元素 (文檔中嵌套層次最深的那個節點) 接收,然後逐級向上傳播到較爲不具體的節點(文檔)。比如單擊了頁面中的<div>元素,那麼這個click事件會按照如下順序逐級向上傳播,直至傳播到 document 對象。
注意:所有現代瀏覽器都支持事件冒泡,但在具體實現上還是有一些差別。IE5.5及更早版本中的事件冒泡會跳過 <html> 元素 (從 <body> 直接跳到 document) 。Firefox、Chrome 和 Safari 則將事件一直冒泡到 window 對象。
2、事件捕獲
事件捕獲的思想是不太具體的節點應該更早接收到事件,而最具體的節點應該最後接收到事件。事件捕獲的用意在於在事件到達預定目標之前捕獲它。前面的例子的事件捕獲過程爲:
3、DOM事件流
“DOM2級事件”規定的事件流包括三個階段:事件捕獲階段、處於目標階段和事件冒泡階段。同樣前面的例子觸發事件的順序爲:
IE9、Opera、Firefox、Chrome和Safari都支持DOM事件流;IE8及更早版本不支持DOM事件流。
三、事件處理程序
事件就是用戶或瀏覽器自身執行的某種動作。而響應某個事件的函數就叫做事件處理程序 (或事件偵聽器)。事件處理程序的名字以 "on" 開頭,比如 click 事件的事件處理程序就是 onclick。爲事件指定處理程序的方式有以下幾種。
1、HTML事件處理程序
在HTML中定義的事件處理程序可以包含要執行的具體動作,也可以調用在頁面其他地方定義的腳本。
<input type="button" value="Click Me" οnclick="alert(this.value)" />
或者
<script type="text/javascript">
function showMessage(){
alert("Hello world!");
}
</script>
<input type="button" value="Click Me" οnclick="showMessage()" />
注意:不能再其中使用未經轉義的HTML語法字符,例如和號(&)、雙引號("")、小於號(<)或大於號(>)。
在HTML中指定事件處理程序有三個缺點。1、存在一個時差問題。因爲用戶可能會在HTML元素一出現在頁面上就觸發相應的事件,但當時的事件處理程序有可能尚不具備執行條件。爲此,很多HTML事件處理程序都會被封裝在一個try-catch塊中,以便錯誤不會浮出水面。2、這樣擴展事件處理程序的作用域在不同瀏覽器中會導致不同結果。不同js引擎遵循的標識符解析規則略有差異,很可能會在訪問非限定對象成員時出錯。3、html與js代碼緊密耦合,更換事件處理程序改動較大。
2、DOM0級事件處理程序
通過 JavaScript 指定事件處理程序的傳統方式,就是將一個函數賦值給一個事件處理程序屬性。這種爲事件處理程序賦值的方法是在第四代Web瀏覽器中出現的,而且至今仍然爲所有現代瀏覽器所支持。原因一是簡單,二是具有跨瀏覽器的優勢。
每個元素都有自己的事件處理程序屬性,如onclick等。可以通過js將一個函數賦值給元素的事件處理程序屬性。在DOM0事件處理程序中,事件處理程序裏的this指向當前元素。
var objlogo=document.getElementById("site_nav_top");
objlogo.οnclick=function(){
alert(this.innerHTML);//代碼改變世界
}
刪除DOM0事件,只需將事件處理程序的值賦爲null即可。
objlogo.οnclick=null;
3、DOM2級事件處理程序
“DOM2級事件”定義了兩個方法,addEventListener() 和 removeEventListener(),它們都接受3個參數:要處理的事件名、作爲事件處理程序的函數和一個布爾值。最後這個布爾值參數如果是 true,表示在捕獲階段調用事件處理程序;如果是 false,表示在冒泡階段調用事件處理程序。addEventListener()可以爲元素添加多個事件處理程序,觸發時會按照添加順序依次調用。removeEventListener()不能移除匿名添加的函數。
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert(this.id);
}, false);
var handler = function(){
alert("Hello world!");
};
btn.addEventListener("click", handler, false);
// 這裏省略了其他代碼
btn.removeEventListener("click", handler, false); // 有效!
以上代碼會先顯示id,再顯示hello world。大多數情況下,都是將事件處理程序添加到事件流的冒泡階段,這樣可以最大限度地兼容各種瀏覽器。IE9、Opera、Firefox、Chrome和Safari都支持DOM2級事件處理程序。
4、IE事件處理程序
IE 實現了與DOM中類似的兩個方法:attachEvent() 和 detachEvent()。這兩個方法接受相同的兩個參數:事件處理程序名稱與事件處理程序函數。由於IE只支持事件冒泡,所以通過 attachEvent() 添加的事件處理程序都會被添加到冒泡階段。
在使用DOM0級方法的情況下,事件處理程序會在其所屬元素的作用域內運行;在使用 attachEvent() 方法的情況下,事件處理程序會在全局作用域中運行,因此 this 等於 window 。並且,與DOM 方法不同的是,這些事件處理程序不是以添加它們的順序執行,而是以相反的順序被觸發。
var btn = document.getElementById("myBtn");
var handler = function(){
alert("Clicked");
};
btn.attachEvent("onclick", handler);
// 這裏省略了其他代碼
btn.detachEvent("onclick", handler);
支持IE事件處理程序的瀏覽器有IE和Opera。
5、跨瀏覽器的事件處理程序
第一個要創建的方法是 addHandler(),它的職責是視情況分別使用DOM0級方法、DOM2級方法或IE方法來添加事件,它屬於一個名叫 EventUtil 的對象。addHandler()方法接受3個參數:要操作的元素、事件名稱和事件處理程序函數。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;
}
}
};
使用EventUtil對象的實例如下:
var btn = document.getElementById("myBtn");
var handler = function(){
alert("Clicked");
};
EventUtil.addHandler(btn, "click", handler);
// 這裏省略了其他代碼
EventUtil.removeHandler(btn, "click", handler);
6、事件回調函數的作用域問題
事件綁定函數時,該函數會以當前元素爲作用域執行。
(1)使用匿名函數
我們爲回調函數包裹一層匿名函數。包裹之後,雖然匿名函數的作用域被指向事件觸發元素,但執行的內容就像直接調用一樣,不會影響其作用域。
(2)使用 bind 方法
使用匿名函數是有缺陷的,每次調用都包裹進匿名函數裏面,增加了冗餘代碼等,此外如果想使用 removeEventListener 解除綁定,還需要再創建一個函數引用。Function 類型提供了 bind 方法,可以爲函數綁定作用域,無論函數在哪裏調用,都不會改變它的作用域。
四、事件對象
在觸發 DOM 上的某個事件時,會產生一個事件對象 event,這個對象中包含着所有與事件有關的信息。
1、DOM中的事件對象
兼容 DOM 的瀏覽器會將一個 event 對象傳入到事件處理程序中。event 對象包含與創建它的特定事件有關的屬性和方法。觸發的事件類型不一樣,可用的屬性和方法也不一樣。不過,所有事件都會有下表列出的成員。
在事件處理程序內部,對象 this 始終等於 currentTarget 的值,而 target 則只包含事件的實際目標。如果直接將事件處理程序指定給了目標元素,則 this 、currentTarget 和 target 包含相同的值。
2、IE中的事件對象
在使用 DOM0 級方法添加事件處理程序時,event 對象作爲 window 對象的一個屬性存在。如果是通過 HTML 特性指定的事件處理程序,那麼還可以通過一個名叫 event 的變量來訪問 event 對象 (與 DOM 中的事件模型相同)。IE 的 event 對象同樣也包含與創建它的事件相關的屬性和方法。
3、跨瀏覽器的事件對象
綜合考慮DOM和IE中的事件對象,寫出跨瀏覽器的事件對象,放在之前的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;
}
}
};
四、事件類型
Web瀏覽器中可能發生的事件有很多類型。如前所述,不同的事件類型具有不同的信息,而 "DOM3級事件" 規定了下列幾類事件:
UI事件:在用戶與頁面上的元素交互時觸發;
鼠標事件:當用戶通過鼠標在頁面上執行操作時觸發;
滾輪事件:當使用鼠標滾輪(或類似設備)時觸發;
文本事件:當在文檔中輸入文本時觸發;
鍵盤事件:當用戶通過鍵盤在頁面上執行操作時觸發;
合成事件:當爲IME(Input Method Editor,輸入法編輯器)輸入字符時觸發;
變動 (mutation) 事件:當底層 DOM 結構發生變化時觸發。
1、UI事件
(1)load事件:load可以用來判斷圖片加載完畢。注意:新創建的圖像元素不是在加載到頁面中才開始下載,而是設置src之後就開始下載。load可以用來判斷js加載完成。<script>元素可以觸發load事件,來判斷動態加載的js文件是否加載完成。它和img不同,必須設置了src屬性並且添加到文檔之後纔會開始下載。
(2).unload事件
(3).resize事件
注意:關於何時會觸發resize事件,不同瀏覽器有不同的機制。IE、Safari、Chrome和Opera會在瀏覽器窗口變化了1像素時就觸發resize事件,然後隨着變化不斷重複觸發。Firefox則只會在用戶停止調整窗口大小時纔會觸發resize事件。
(4).scroll事件
2、焦點事件
焦點事件會在頁面元素獲得或失去焦點時觸發,利用這些事件並與document.hasFocus()方法及document.activeElement屬性配合,可以知曉用戶在頁面上的行蹤。有以下6個焦點事件:blur、DOMFocusIn、DOMFocusOut、foucs、focusin、focusout。
3、鼠標與滾輪事件
DOM3級事件定義了9個鼠標事件:click、dblclick、mousedown、mouseenter、mouseleave、mousemove、mouseout、mouseover、mouseup。
鼠標事件中還有一類滾輪事件mousewheel事件。
(1).客戶區座標位置:clientX和clientY屬性。
(2).頁面座標位置:pageX和pageY屬性。
(3).屏幕座標位置:screenX和screenY屬性。
(4).修改鍵:shiftKey、ctrlKey、altKey、metaKey。
(5).相關元素:DOM通過event對象的relatedTarget屬性提供了相關元素的信息。而在IE中,mouseover事件觸發時,fromElement保存了相關元素,mouseout事件觸發時,toElement保存了相關元素。
(6).鼠標按鈕
(7).更多的事件信息
(8).鼠標滾輪事件:mousewheel事件中包含wheelDelta屬性,Firefox支持類似的DOMMouseScroll事件,包含detail屬性。
4、鍵盤與文本事件
DOM3級含有3個鍵盤事件:keydown、keypress、keyup。
1.鍵碼:keyCode屬性
2.字符編碼:charCode屬性
3.DOM3級變化:新屬性(key、char、location屬性:表示按下了什麼位置上的鍵),getModifierState()方法。
4.textinput事件:data屬性:表示用戶輸入的字符(而非字符編碼)、inputMethod屬性:表示把文本輸入到文本框中的方式。
5、複合事件:用以處理IME的輸入序列的一類事件。
6、變動事件:刪除節點、插入節點等。
7、HTML5事件
1.contextmenu事件:用以表示何時應該顯示上下文菜單。
2.beforeunload事件:讓開發人員有可能在頁面卸載前阻止這一操作。
3.DOMContentLoaded事件:在形成完整的DOM樹之後觸發。
4.readystatechange事件:提供與文檔或元素的加載狀態有關的信息。
5.pageshow和pagehide事件:pageshow事件在頁面顯示時觸發,pagehide事件在瀏覽器卸載頁面時觸發。
6.hashchange事件:以便在URL的參數列表發生變化時通知開發人員。
8、設備事件
1.orientationchange事件
2.MozOrientation事件
3.deviceorientation事件
4.devicemotion事件
9、觸摸與手勢事件
1.觸摸事件
2.手勢事件
五、內存和性能
1.事件委託
事件委託可以解決頁面中事件處理程序過多的問題。事件委託利用了事件冒泡,只指定一個事件處理程序,就可以處理某一類型的所有事件。
EventUtil.addHandler(document.body, 'click', function (event) {
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
switch (target.id) {
case 'site_nav_top':
alert('口號');
break;
case 'nav_menu':
alert('點擊菜單');
EventUtil.preventDefault(event);
break;
case 'editor_pick_lnk':
alert('推薦區');
EventUtil.preventDefault(event);
break;
}
});
2.移除事件處理程序
如果內存中保留大量無用的事件處理程序,會影響性能。所以一定要在不需要的時候及時移除事件處理程序。尤其注意以下情況:使用innerHTML刪除帶有事件處理程序的元素時,要先將事件處理程序設置爲null。使用委託也可以解決這個問題,不直接將事件加載會被innerHTML替換的元素,而是將事件賦給其父元素,這樣就可以避免了。卸載頁面時,最好手工清除所有的事件處理程序。
六、DOM中的事件模擬
事件經常由用戶操作或通過其他瀏覽器功能來觸發,也可以使用JS在任意時刻來觸發特定的事件,而此時的事件就如同瀏覽器創建的一樣。在測試Web應用程序,模擬觸發事件是一種極其有用的技術。
1. DOM中的事件
模擬分三步:
使用document.createEvent()創建event對象。通過賦值不同的參數,可以模擬不同的事件類型
初始化event對象;
觸發事件,調用dispatchEvent()方法,所有支持事件的DOM節點都可以支持這個方法。
function simulateClick() {
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent("click", true, true, window,
0, 0, 0, 0, 0, false, false, false, false, 0, null);
var cb = document.getElementById("checkbox");
var canceled = !cb.dispatchEvent(evt);
if(canceled) {
// A handler called preventDefault
alert("canceled");
} else {
// None of the handlers called preventDefault
alert("not canceled");
}
}
2. 模擬鼠標事件
首先createEvent()方法傳入參數是“MouseEvent”來模擬鼠標事件。返回的event對象有一個initMouseEvent()方法,用來初始化事件信息。該方法有15個參數:
type(字符串):要觸發的事件類型,比如“click”。
bubbles(bool):事件是否冒泡。一般設爲true。
cancelable(bool):事件是否可以取消。一般設爲true。
view:與事件關聯的視圖,一般設置爲document.defaultView。
detail(整數):與事件有關的詳細信息,一般設置爲0.
screenX:事件相對於屏幕的X座標。
screenY:事件相對於屏幕的Y座標。
clientX:事件相對於視口的X座標。
clientY:事件相對於視口的Y座標。
ctrlKey(bool):是否俺下了Ctrl鍵,默認爲false。
altKey:是否按下了alt鍵,默認false。
shiftKey:是否按下了shift鍵,默認false。
metaKey:是否按下了meta鍵,默認false.
button(整數):按下了哪個鼠標鍵,默認0。
relatedTarget:與事件相關的對象,一般爲null.只在模擬mouseover和mouseout的時候會用到。
最後給DOM元素調用dispatchEvent()方法來觸發事件。
var objmenu = document.getElementById('nav_menu');
objmenu.onclick = function () {
console.log('menu');
}
document.body.onclick = function () {
console.log('body');
}
var event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, document.defaultView, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
for (var i = 0; i < 5; i++) {
objmenu.dispatchEvent(event);
}
3. 自定義DOM事件
DOM3中支持自定義DOM事件。要創建新的自定義事件,可以調用document.createEvent()方法,返回的對象有一個initCustomEvent()方法,包含四個參數:
type:觸發事件的類型;
bubbles:事件是否冒泡;
cancelable:事件是否可以取消;
detail:任意值,保存在event.detail屬性中。
最後在DOM元素調用dispatchEvent()方法觸發事件。
var objmenu = document.getElementById('nav_menu');
EventUtil.addHandler(objmenu, 'myevent', function (event) {
console.log('menu' + event.detail);
});
EventUtil.addHandler(document.body, 'myevent', function (event) {
console.log('body' + event.detail);
})
if (document.implementation.hasFeature('CustomEvents', '3.0')) {
var event = document.createEvent('CustomEvent');
event.initCustomEvent('myevent', true, true, '測試事件detail');
for (var i = 0; i < 5; i++) {
objmenu.dispatchEvent(event);
}
}
自定義事件的函數有 Event、CustomEvent 和 dispatchEvent。直接自定義事件,使用 Event 構造函數;CustomEvent 可以創建一個更高度自定義事件,還可以附帶一些數據,具體用法如下:
var myEvent = new CustomEvent(eventname, options);
其中 options 可以是:
{
detail: {
...
},
bubbles: true,
cancelable: false
}
其中 detail 可以存放一些初始化的信息,可以在觸發的時候調用。其他屬性就是定義該事件是否具有冒泡等等功能。內置的事件會由瀏覽器根據某些操作進行觸發,自定義的事件就需要人工觸發。dispatchEvent 函數就是用來觸發某個事件:
element.dispatchEvent(customEvent);
上面代碼表示,在 element 上面觸發 customEvent 這個事件。結合起來用就是:
// add an appropriate event listener
obj.addEventListener("cat", function(e) { process(e.detail) });
// create and dispatch the event
var event = new CustomEvent("cat", {"detail":{"hazcheeseburger":true}});
obj.dispatchEvent(event);
使用自定義事件需要注意兼容性問題,而使用 jQuery 就簡單多了:
// 綁定自定義事件
$(element).on('myCustomEvent', function(){});
// 觸發事件
$(element).trigger('myCustomEvent');
此外,你還可以在觸發自定義事件時傳遞更多參數信息:
$( "p" ).on( "myCustomEvent", function( event, myName ) {
$( this ).text( myName + ", hi there!" );
});
$( "button" ).click(function () {
$( "p" ).trigger( "myCustomEvent", [ "John" ] );
});