JavaScript事件模型及事件代理
事件模型
JavaScript事件使得網頁具有互動和交互性,咱們應該對其深刻了解以便開發工做,在各式各樣的瀏覽器中,JavaScript事件模型主要分爲3種:原始事件模型、DOM2事件模型、IE事件模型。java
1.原始事件模型(DOM0級)
這是一種被全部瀏覽器都支持的事件模型,對於原始事件而言,沒有事件流,事件一旦發生將立刻進行處理,有兩種方式能夠實現原始事件:node
(1)在html代碼中直接指定屬性值:<button id="demo" type="button" onclick="doSomeTing()" /> web
(2)在js代碼中爲 document.getElementsById("demo").onclick = doSomeTing()ajax
優勢:全部瀏覽器都兼容chrome
缺點:1)邏輯與顯示沒有分離;2)相同事件的監聽函數只能綁定一個,後綁定的會覆蓋掉前面的,如:a.onclick = func1; a.onclick = func2;將只會執行func2中的內容。3)沒法經過事件的冒泡、委託等機制(後面會講到)完成更多事情。瀏覽器
由於這些缺點,雖然原始事件類型兼容全部瀏覽器,但仍不推薦使用。性能優化
2.DOM2事件模型
此模型是W3C制定的標準模型,現代瀏覽器(IE6~8除外)都已經遵循這個規範。W3C制定的事件模型中,一次事件的發生包含三個過程:app
(1).事件捕獲階段,(2).事件目標階段,(3).事件冒泡階段。以下圖所示
事件捕獲:當某個元素觸發某個事件(如onclick),頂層對象document就會發出一個事件流,隨着DOM樹的節點向目標元素節點流去,直到到達事件真正發生的目標元素。在這個過程當中,事件相應的監聽函數是不會被觸發的。
事件目標:當到達目標元素以後,執行目標元素該事件相應的處理函數。若是沒有綁定監聽函數,那就不執行。
事件冒泡:從目標元素開始,往頂層元素傳播。途中若是有節點綁定了相應的事件處理函數,這些函數都會被一次觸發。
IE 5.5: div -> body -> document
IE 6.0: div -> body -> html -> document
Mozilla 1.0: div -> body -> html -> document -> window
全部的事件類型都會經歷事件捕獲可是隻有部分事件會經歷事件冒泡階段,例如submit事件就不會被冒泡。
事件的傳播是能夠阻止的:
• 在W3c中,使用stopPropagation()方法
• 在IE下設置eve.cancelBubble = true;
在捕獲的過程當中stopPropagation();後,後面的冒泡過程就不會發生了。
標準的事件監聽器該如何綁定:
addEventListener("eventType","handler","true|false");其中eventType指事件類型,注意不要加‘on’前綴,與IE下不一樣。第二個參數是處理函數,第三個即用來指定是否在捕獲階段進行處理,通常設爲false來與IE保持一致(默認設置),除非你有特殊的邏輯需求。監聽器的解除也相似:removeEventListner("eventType","handler","true!false");
3.IE事件模型
IE不把該對象傳入事件處理函數,因爲在任意時刻只會存在一個事件,因此IE把它做爲全局對象window的一個屬性,爲求證其真僞,使用IE8執行代碼alert(window.event),結果彈出是null,說明該屬性已經定義,只是值爲null(與undefined不一樣)。難道這個全局對象的屬性是在監聽函數裏才加的?因而執行下面代碼:
window.onload = function (){alert(window.event);}
setTimeout(function(){alert(window.event);},2000);
結果第一次彈出【object event】,兩秒後彈出依然是null。因而可知IE是將event對象在處理函數中設爲window的屬性,一旦函數執行結束,便被置爲null了。IE的事件模型只有兩步,先執行元素的監聽函數,而後事件沿着父節點一直冒泡到document。冒泡已經講解過了,這裏不重複。IE模型下的事件監聽方式也挺獨特,綁定監聽函數的方法是:attachEvent( "eventType","handler"),其中evetType爲事件的類型,如onclick,注意要加’on’。解除事件監聽器的方法是 detachEvent("eventType","handler" )
IE的事件模型已經能夠解決原始模型的三個缺點,但其本身的缺點就是兼容性,只有IE系列瀏覽器才能夠這樣寫。
以上就是3種事件模型,在咱們寫代碼的時候,爲了兼容ie,一般使用如下寫法:
var demo = document.getElementById('demo');
if(demo.attachEvent){
demo.attachEvent('onclick',func);
}else{
demo.addEventListener('click',func,false);
}
event詳解
上面已經講解了3種事件模型,事件,大部分狀況下指的是用戶的鼠標動做和鍵盤動做,如點擊、移動鼠標、按下某個鍵,爲何說大部分呢,由於事件不僅僅只有這兩部分,還有其餘的例如document的load和unloaded。那麼事件在瀏覽器中,到底包含哪些信息呢?
事件被封裝成一個event對象,包含了該事件發生時的全部相關信息(event的屬性)以及能夠對事件進行的操做(event的方法)。
我爲下圖中的button綁定了一個點擊事件,而後將event輸出到控制檯:
能夠看到是一個MouseEvent對象,包含了一系列屬性,如鼠標點擊的位置等。那麼敲擊鍵盤時產生的event對象和它同樣嗎?看看就知道:
能夠看到是一個KeyboardEvent對象,屬性跟上面的也不太同樣,如沒有clientX/Y(敲鍵盤怎麼能獲取到鼠標的位置呢)。無論是MouseEvent仍是KeyboardEvent或是其餘類型,都是繼承自一個叫Event的類。
event對象經常使用屬性、方法:
1. 事件定位相關屬性
若是你細細看了MouseEvent對象裏的屬性,必定發現了有不少帶X/Y的屬性,它們都和事件的位置相關。具體包括:x/y、clientX/clientY、pageX/pageY、screenX/screenY、layerX/layerY、offsetX/offsetY 六對。爲何有這麼多X-Y啊?不要着急,做爲一個web開發者,你應該瞭解各瀏覽器之間是有差別的,這些屬性都有各自的意思:
x/y與clientX/clientY值同樣,表示距瀏覽器可視區域(工具欄除外區域)左/上的距離;
pageX/pageY,距頁面左/上的距離,它與clientX/clientY的區別是不隨滾動條的位置變化;
screenX/screenY,距計算機顯示器左/上的距離,拖動你的瀏覽器窗口位置能夠看到變化;
layerX/layerY與offsetX/offsetY值同樣,表示距有定位屬性的父元素左/上的距離。
下面列出了各屬性的瀏覽器支持狀況。(+支持,-不支持)
offsetX/offsetY | W3C- | IE+ | Firefox- | Opera+ | Safari+ | chrome+ |
x/y | W3C- | IE+ | Firefox- | Opera+ | Safari+ | chrome+ |
layerX/layerY | W3C- | IE- | Firefox+ | Opera- | Safari+ | chrome+ |
pageX/pageY | W3C- | IE+- | Firefox+ | Opera+ | Safari+ | chrome+ |
clientX/clientY | W3C+ | IE+ | Firefox+ | Opera+ | Safari+ | chrome+ |
screenX/screenY | W3C+ | IE+ | Firefox+ | Opera+ | Safari+ | chrome+ |
注意:該表摘自其餘文章,我未作所有驗證,可是最新版本的現代瀏覽器,這些屬性貌似是都支持了,爲了更好的兼容性,一般選擇W3C支持的屬性。
2.其餘經常使用屬性
target:發生事件的節點;
currentTarget:當前正在處理的事件的節點,在事件捕獲或冒泡階段;
timeStamp:事件發生的時間,時間戳。
bubbles:事件是否冒泡。
cancelable:事件是否能夠用preventDefault()方法來取消默認的動做;
keyCode:按下的鍵的值;
3. event對象的方法
event. preventDefault()//阻止元素默認的行爲,如連接的跳轉、表單的提交;
event. stopPropagation()//阻止事件冒泡
event.initEvent()//初始化新事件對象的屬性,自定義事件會用,不經常使用
event. stopImmediatePropagation()//能夠阻止掉同一事件的其餘優先級較低的偵聽器的處理(這貨表示沒用過,優先級就不說明了,谷歌或者問度娘吧。)
4. 阻止事件的默認行爲,例如click <a>後的跳轉~
• 在W3c中,使用preventDefault()方法;
• 在IE下設置window.event.returnValue = false;
event.target與event.currentTarget他們有什麼不一樣?
target在事件流的目標階段;currentTarget在事件流的捕獲,目標及冒泡階段。只有當事件流處在目標階段的時候,兩個的指向纔是同樣的, 而當處於捕獲和冒泡階段的時候,target指向被單擊的對象而currentTarget指向當前事件活動的對象(通常爲父級)。
事件觸發器
前面提到的事件都是依靠用戶或者瀏覽器自帶事件去觸發的,好比click是用戶點擊事件目標觸發,load是指定元素已載入的時候瀏覽器的行爲事件,等等,若是隻有在這些條件下才能觸發事件,那麼咱們的自定義事件如何觸發呢?
事件觸發器就是用來觸發某個元素下的某個事件,固然也能夠用來觸發自定義事件IE下fireEvent方法,現代瀏覽器(chrome,firefox等)有dispatchEvent方法。
這裏先介紹下自定義事件(事件模擬):
自定義事件
想要實現一個自定義事件,須要通過下面幾步:
1.createEvent(eventType)
事件被封裝成一個event對象,這在上面已經說過了,咱們想要自定義一個事件,js中有這麼一個方法createEvent(eventType),見名知義,顯然是用於「創造」一個事件的,沒錯,要想自定義事件,首先,咱們得「創造」一個事件。
參數:eventType 共5種類型:
Events :包括全部的事件.
HTMLEvents:包括 'abort', 'blur', 'change', 'error', 'focus', 'load', 'reset', 'resize', 'scroll', 'select',
'submit', 'unload'. 事件
UIEevents :包括 'DOMActivate', 'DOMFocusIn', 'DOMFocusOut', 'keydown', 'keypress', 'keyup'.
間接包含 MouseEvents.
MouseEvents:包括 'click', 'mousedown', 'mousemove', 'mouseout', 'mouseover', 'mouseup'.
MutationEvents:包括 'DOMAttrModified', 'DOMNodeInserted', 'DOMNodeRemoved',
'DOMCharacterDataModified', 'DOMNodeInsertedIntoDocument',
'DOMNodeRemovedFromDocument', 'DOMSubtreeModified'.
2. 在createEvent後必須初始化,爲你們介紹5種對應的初始化方法
HTMLEvents 和 通用 Events:
initEvent( 'type', bubbles, cancelable )
UIEvents:
initUIEvent( 'type', bubbles, cancelable, windowObject, detail )
MouseEvents:
initMouseEvent( 'type', bubbles, cancelable, windowObject, detail, screenX, screenY,
clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget )
MutationEvents :
initMutationEvent( 'type', bubbles, cancelable, relatedNode, prevValue, newValue,
attrName, attrChange )
這裏重點介紹MouseEvents(鼠標事件模擬):
鼠標事件能夠經過建立一個鼠標事件對象來模擬(mouse event object),而且授予他一些相關信息,建立一個鼠標事件經過傳給createEvent()方法一個字符串「MouseEvents」,來建立鼠標事件對象,以後經過iniMouseEvent()方法來初始化返回的事件對象,iniMouseEvent()方法接受15參數,參數以下:
type string類型 :要觸發的事件類型,例如‘click’。
bubbles Boolean類型:表示事件是否應該冒泡,針對鼠標事件模擬,該值應該被設置爲true。
cancelable bool類型:表示該事件是否可以被取消,針對鼠標事件模擬,該值應該被設置爲true。
view 抽象視圖:事件授予的視圖,這個值幾乎全是document.defaultView.
detail int類型:附加的事件信息這個初始化時通常應該默認爲0。
screenX int類型 : 事件距離屏幕左邊的X座標
screenY int類型 : 事件距離屏幕上邊的y座標
clientX int類型 : 事件距離可視區域左邊的X座標
clientY int類型 : 事件距離可視區域上邊的y座標
ctrlKey Boolean類型 : 表明ctrol鍵是否被按下,默認爲false。
altKey Boolean類型 : 表明alt鍵是否被按下,默認爲false。
shiftKey Boolean類型 : 表明shif鍵是否被按下,默認爲false。
metaKey Boolean類型: 表明meta key 是否被按下,默認是false。
button int類型: 表示被按下的鼠標鍵,默認是零.
relatedTarget (object) : 事件的關聯對象.只有在模擬mouseover 和 mouseout時用到。
若是你想要了解更多事件模擬參數詳解,請查看這篇文章,http://www.cnblogs.com/MrBackKom/archive/2012/06/26/2564501.html。或者查看《javascript高級程序設計》的模擬事件章節
3. 在初始化完成後就能夠隨時觸發須要的事件了,爲你們介紹targetObj.dispatchEvent(event)使targetObj對象的event事件觸發。(IE上請用fireEvent方法)
4. 例子
//例子1 當即觸發鼠標被按下事件
1
2
3
4
|
var
fireOnThis = document.getElementById(
'demo'
);
var
evObj = document.createEvent(
'MouseEvents'
);
evObj.initMouseEvent(
'click'
,
true
,
true
, window, 1, 12, 345, 7, 220,
false
,
false
,
true
,
false
, 0,
null
);
fireOnThis.dispatchEvent(evObj);
|
//例子2 考慮兼容性的一個鼠標移動事件
1
2
3
4
5
6
7
8
9
|
var
fireOnThis = document.getElementById(
'someID'
);
if
( document.createEvent ) {
var
evObj = document.createEvent(
'MouseEvents'
);
evObj.initEvent(
'mousemove'
,
true
,
false
);
fireOnThis.dispatchEvent(evObj);
}
else
if
( document.createEventObject )
{
fireOnThis.fireEvent(
'onmousemove'
);
}
|
事件代理
傳統的事件處理中,咱們爲每個須要觸發事件的元素添加事件處理器,可是這種方法將可能會致使內存泄露或者性能降低(特別是經過ajax獲取數據後重復綁定事件,總之,越頻繁風險越大)。事件代理在js中是一個很是有用但對初學者稍難理解的功能,咱們將事件處理器綁定到一個父級元素上,避免了頻繁的綁定多個子級元素,依靠事件冒泡機制與事件捕獲機制,子級元素的事件將委託給父級元素。事件冒泡與捕獲在上面事件模型中已經講解過。
有了事件捕獲和冒泡的認識後,下面舉例說明事件代理:
假設咱們有一個列表,列表中的每個li和li中的span都須要綁定某個事件處理函數。以下代碼:
1
2
3
4
5
6
7
8
|
<ul id=
"parent-ul"
>
<li><span>Item 1</span></li>
<li><span>Item 2</span></li>
<li><span>Item 3</span></li>
<li><span>Item 4</span></li>
<li><span>Item 5</span></li>
<li><span>Item 6</span></li>
</ul>
|
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
|
~(
function
() {
var
ParentNode = document.querySelector(
"#parent-ul"
);
var
targetNodes = ParentNode.querySelectorAll(
"li"
);
var
spanNodes = ParentNode.querySelectorAll(
"span"
);
//綁定事件處理函數
for
(
var
i=0, l = targetNodes.length; i < l; i++){
addListenersToLi(targetNodes[i]);
}
for
(
var
i=0, l = spanNodes.length; i < l; i++){
addListenersToSpan(spanNodes[i]);
}
//事件處理函數
function
addListenersToLi(targetNode){
targetNode.onclick =
function
targetClick(e){
if
(e.target && e.target.nodeName.toUpperCase() ==
"LI"
) {
console.log(
"當你看見個人時候,LI點擊事件已經生效!"
);
}
};
}
function
addListenersToSpan(targetNode){
targetNode.onclick =
function
targetClick(e){
if
(e.target && e.target.nodeName.toUpperCase() ==
"SPAN"
) {
console.log(
"當你看見個人時候,SPAN點擊已經生效!"
);
}
};
}
})();
|
這裏爲li和span元素都添加了onclick事件處理函數,可是若是這些li和span有可能被刪除或者新增,那麼老是須要爲新增的li、span元素從新綁定事件,這種寫法使得咱們很苦惱,除了開頭提到的問題外,增長了代碼量並且代碼看上去不太整潔了,那麼使用事件代理會怎麼樣呢?以下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
~(
function
() {
// 獲取li的父節點,併爲其添加一個click事件
document.getElementById(
"parent-ul"
).addEventListener(
"click"
,
function
(e) {
// 檢查事件源e.targe是否爲span
if
(e.target && e.target.nodeName.toUpperCase() ==
"SPAN"
) {
// 真正的處理過程在這裏
console.log(
"當你看見個人時候,SPAN事件代理已經生效!"
);
}<br>
//檢查事件源e.target是否爲li
if
(e.target && e.target.nodeName.toUpperCase() ==
"LI"
) {
// 真正的處理過程在這裏
console.log(
"當你看見個人時候,LI事件代理已經生效!"
);
}
});
})();
|
咱們改變了思路,爲li、span的父級元素即id爲parent-ul的ul元素添加了一click事件,當點擊事件發生時,咱們能夠經過e.target捕獲事件目標,並經過e.target.nodeName.toUpperCase== "LI"來判斷事件目標是否爲li(span同理),如是那麼執行相應的事件處理程序。使用這樣的方式有利於解決前面提到的一些問題:
1.最直接的就是,代碼更整潔了,並且可讀性更強。
2.對於動態化的頁面(如本例,li、span會新增和刪除),不用頻繁的綁定事件,減小了內存泄露的機率。
注意:不是全部的事件都能冒泡的。blur、focus、load和unload不能像其它事件同樣冒泡。事實上blur和focus能夠用事件捕獲而非事件冒泡的方法得到(在IE以外的其它瀏覽器中)。
在管理鼠標事件的時候有些須要注意的地方。若是你的代碼處理mousemove事件的話你趕上性能瓶頸的風險可就大了,由於mousemove事件觸發很是頻繁。而mouseout則由於其怪異的表現而變得很難用事件代理來管理。
jq事件代理:jq爲提供了delegate()函數處理事件代理,這裏很少介紹,我的在工做中使用on()函數解決一些事件代理問題(使用更方便),解決上訴例子的代碼以下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
~(
function
() {
$(
"#parent-ul"
).on(
"click"
,
"li,span"
,
function
(e) {
if
($(
this
)[0].nodeName==
"SPAN"
) {
// 真正的處理過程在這裏
console.log(
"當你看見個人時候,SPAN事件代理已經生效!"
);
//這裏要阻止冒泡,否則點擊span時會觸發li的事件
e.stopPropagation();
}
if
($(
this
)[0].nodeName==
"LI"
) {
// 真正的處理過程在這裏
console.log(
"當你看見個人時候,LI事件代理已經生效!"
);
}
});
})();
|
若是你使用jq,推薦使用on()方法。