JS編程中經常接觸到DOM事件,雖然只是事件而已,可是處於歷史緣由,它是足夠複雜的,也足夠強大的,因爲本身以前老是對DOM事件的一些概念模糊不清,因此這裏來作一下總結啦html
咱們常聽到DOM0級事件,DOM1級事件,DOM2級事件處理,其實這裏的DOM0,DOM1,DOM2和DOM級別是密切相關的。node
最開始的時候,IE4和Netscape等瀏覽器各自實現本身的一套DOM api,沒有一個統一的標準,對於開發者和用戶來講是一件頭疼的事,因而,1998年,W3C綜合了各家的現有API,制定了DOM1級(DOM Level 1)標準。DOM1級比較簡潔,由兩個模塊組成:DOM核心(DOM Core)和DOM HTML,其中各個事件相關的信息做爲方法包含在在DOM元素中進行說明的,詳細能夠了解W3C的相關章節.編程
DOM2級的目標要寬泛不少,引入了多個新模塊,其中包括DOM Events,即在DOM2的時候,單獨把事件做爲一個模塊,並針對DOM1級事件部分做了很大的升級,再也不只是簡單的告訴咱們有哪些事件能夠用,而是使事件的標準更加詳細,如新增了事件流,事件捕獲,事件冒泡,事件取消等機制和規範。api
DOM Level 3並無對事件作任何修訂(多是由於認爲DOM Events已經足夠成熟了吧),因此咱們知道如今用的仍是DOM Level 2的事件標準。瀏覽器
到這裏能夠作一下總結,DOM API 是有DOM1級,DOM2級,DOM3級三個標準的,與之對應的每個標準中DOM事件相關的部分咱們叫作DOM1級事件處理,DOM2級事件處理(剛纔也說了,DOM3級事件處理不存在),那麼標準是從DOM Level 1開始的,咱們說的DOM0又是什麼呢?能夠說這只是公認的一種說法,即在有事實標準以前的事件處理,咱們叫作DOM0級事件處理。bash
由於DOM1只是對以前各大廠商的DOM api作了如下整理而實施的標準,因此一般咱們認爲DOM1的事件處理和DOM0的事件處理是同樣的。函數
事件處理程序就是響應某個事件的函數,DOM中的事件處理程序有多種方式,大概能夠分爲如下三種類型。工具
<button onclick="alert(hello world!)"></hello>
複製代碼
像上述代碼這樣,直接將事件函數寫到HTML中元素的屬性上,就是HTML事件處理程序,這裏雙引號中的部分是事件觸發後要執行的代碼,它其實是由JS引擎由eval()
調用的,因此它是全局做用域。ui
這樣的事件處理有一個明顯的缺點,即當JS代碼太複雜時,將大段JS代碼卸載HTML中顯然不合適,因而有了下面這種寫法:this
<button onclick="doSomething()"></hello>
複製代碼
這樣雖然解決了嵌套代碼過長的問題,但又引來了另外一個問題,即時差問題—,若是用戶在界面剛出現就進行點擊,而JS尚未加載好的話,就會報錯。
此外,很重要的一點是,這種寫法,一個函數的改變,可能同時須要js和html的改變,嚴重違背了輕耦合的原則,綜上,咱們有了DOM0級事件處理。
<script>
var btn=document.getElementById("#btn");
btn.onclick=function(){
alert(hello world!)
}
</script>
複製代碼
能夠看到,這種方式中能夠把事件處理相關部門都放到js中,而且這裏的事件處理程序是做爲btn對象的方法的,是局部做用域。
可是如今,我依然面臨着問題,若是有對這個元素的單擊事件添加兩個處理函數,這個就沒法幫我實現了,並且即便不須要添加多個處理函數,我也不太敢輕易的添加事件,除非我很是肯定,別人寫代碼時不會涉及到這部分(由於一不當心可能會覆蓋他人以前對這個元素的該事件添加的處理函數)。
進一步規範後,有了DOM2級事件處理程序,咱們能夠經過相似以下代碼,對一個元素的同一個事件添加多個處理程序
var btn=document.getElementById("#btn");
btn.addEventListener("click",function(){
alert(hello world!)
})
btn.addEventListener("click",function(){
alert(hello world2!)
})
</script>
複製代碼
經過DOM2級的addEventListener
方法咱們能夠實現綁定多個事件處理程序,但要注意的是一樣的事件和事件流機制下相同的方法只會觸發一次,即相同的方法會發生覆蓋。 等等,這裏的事件流又是什麼呢?
funcgrand(),funcparent(),funcchild()
,那麼當我在兒子上單擊時,哪一個函數會被觸發呢?
首先來分析一下,若是說直觀感覺是在兒子元素上發生的單擊事件,因此應該觸發funcchild()
,但細細想來這樣是不妥的,由於兒子元素自己就是父親元素甚至爺爺元素的一部分,因此說是否是至關於也在父親和爺爺元素上發生了單擊事件呢?答案是是的,這種狀況下三個元素綁定的對應事件的函數都會被瀏覽器觸發,那麼問題又來啦,既然三個函數都會被觸發,那麼它們應該以什麼順序被觸發呢,是自上到下呢,仍是自下到上呢?
這個問題也就是咱們常說的事件流了,即元素從頁面中接收事件的順序,也即事件在頁面中的傳播順序。
W3C對這個問題給了咱們一個答案,就是均可以,既能夠自上而下依次觸發,又能夠自下而上觸發,具體順序由咱們本身而定(之因此支持這兩種方式,是爲了與以前瀏覽器的實現兼容,由於早期IE事件傳播方向爲從上至下,而Netscape 則從下至上)。
實際上,以前咱們提到的addEventListener
還有第三個參數,能夠爲true
或false
.當第三個參數爲true
時,綁定的是捕獲階段的事件,在捕獲階段,事件是由上到下依次觸發的,反之當第三個參數爲false
時,綁定的是冒泡階段的事件,在冒泡階段,事件是由下到上觸發的。
W3C規定,當事件發生時,最早通知window,而後是document,由上到下依次進入知道最底層的被觸發的那個元素(也就是目標元素,一般的event.target
的值)爲止,這個過程就是捕獲。 以後,事件會從目標元素開始,冒泡,由下至上逐層傳遞至window,這個過程就是冒泡。
因此,捕獲是會比冒泡先執行的
正如事件捕獲和事件冒泡提到的,事件程序可能會在兩個階段中被執行,即捕獲中和冒泡中,當一個事件添加了兩個處理函數,一個指定了參數true
,一個指定的參數false
,則它們都會被執行,且參數爲true
的那個先執行,由於是捕獲階段先發生.
可是有一個例外,即若是事件函數被添加在了目標元素自己上,如以前的例題中的兒子元素上被綁定了兩個單擊事件函數,一個第三個參數是true
,一個第三個參數是false
,則它們的實際執行順序是不受第三個參數控制的,而只是單純的和添加事件的順序有關(先addEventListener
的先執行),這個多是和處於目標階段有關(目標階段和捕獲階段和冒泡階並稱爲三大階段,因此說目標階段中要把捕獲和冒泡的思想排除?真正的順序是捕獲—>目標階段->冒泡吧?)
對於IE來講,在IE9以前,必須使用attachEvent
而不是標準方法addEventListener
,IE事件處理程序中有相似於DOM2級事件處理程序的2個方法attachEvent
和detachEvent
它們都接收兩個參數:
事件處理程序名稱,如 onclick
,onmounseover
,注意,這裏是事件處理程序名稱,而不是事件名稱,要有前綴on
事件處理程序函數
不像DOM2級事件處理程序同樣,它們不接收第三個參數,由於IE8及更早版本只支持冒泡事件流(沒有捕獲階段)
在IE8中,事件執行的順序不是添加的順序而是添加順序的相反順序,而在IE6,7中 事件執行的順序是隨機的,和添加順序無關。
使用attachEvent方法還有個缺點是,this的值會變成window對象的引用而不是觸發事件的元素。
就像上述提到的,老的IE瀏覽器的事件處理程序不一樣於標準的DOM2事件處理,因此爲了兼容各瀏覽器的事件處理,咱們能夠用一個封裝的工具函數來實現通用的添加,移除事件。
var EventUtil={
addEventHandler: function(type,element,handler){
if(element.addEventListener){
element.addEventListener(type,handler,false);
}else if(element.attachEvent){
element.attachEvent("on"+type,element);
}else{
element["on"+type]=handler;
}
},
removeEventHandler: function(type,element,handler){
if(element.removeEventListener){
element.removeEventListener(type,handler,false);
}else if(element.detachEvent){
element.detachEvent("on"+type,element);
}else{
element["on"+type]=null;
}
}
}
複製代碼
事件對象是用來記錄一些事件發生時的相關信息的對象,但事件對象只有事件發生時纔會產生,而且只能在事件處理函數內部訪問,在全部事件處理函數結束後,事件對象會被銷燬。
標準的Event對象屬性主要有如下幾個:
- bubbles 布爾值,表示事件是不是冒泡類型
- cancelable 布爾值,表示事件是否能夠取消默認動做
- currentTarget 當前目標元素,即添加當前事件處理程序的元素
- target 實際目標元素,即實際觸發事件的元素
- type 返回當前事件的名稱
- eventPhase 事件傳播的當前階段,1表示捕獲階段
標準的Event對象的方法主要有如下幾個:
- preventDefault() 通知瀏覽器不要執行該事件的默認動做,經常使用於阻止連接的跳轉,表單的提交,等標籤的默認行爲
- stopPropagation() 冒泡階段下,阻止事件的繼續向上冒泡
和事件處理程序同樣,事件對象的屬性和方法也存在兼容性問題。
window.event
來獲取,解決方式以下:function getEvent(event){
event = event || window.event
}
function hander(event){
event = getEvent(event)
...
}
複製代碼
IE瀏覽器的event
事件沒有preventDefault()
這個方法,可是能夠經過設置event
的returnValue
值爲false
來達到一樣的效果,以下:
window.event.returnValue=false;
複製代碼
IE瀏覽器的event
對象也沒有stopPropagation()
方法,但能夠設置cancelBubble
屬性爲true
,阻止事件的繼續傳播,以下:
window.event.cancelBubble=true;
複製代碼
事件委託就是利用事件冒泡,只需指定一個事件處理程序,就能夠管理某一類型的全部事件,經過事件委託,能夠作到經過在祖先元素添加一個事件處理程序,就能夠控制其子孫元素的某些行爲。
需求是未ul下的全部li添加click事件對應的行爲處理,在沒有用事件委託以前,代碼是着這樣的:
<ul>
<li>列表項1</li>
<li>列表項2</li>
<li>列表項3</li>
</ul>
<script>
var list=document.getElementsByTagName("li");
for(i=0;i<list.length;i++){
list[i].onclick=function(){
alert("我是"+e.target);
}
}
</script>
複製代碼
目前確實達到了,可以全部li都能對click事件有所響應了,但若是再添加一個添加列表項的按鈕呢?當動態的添加列表項時,列表項元素被添加了,但是新添加的節點是沒有綁定事件的(除非在添加元素時再加上綁定事件的邏輯),到這裏,咱們發現了問題所在:
- 在全部元素上一一添加事件綁定會致使頻繁的操做DOM獲取元素,同時多個元素各自監聽本身的事件,都會增長瀏覽器的消耗
- 在頁面中動態添加元素時,還須要從新走一遍添加監聽事件的邏輯才能使新元素可以響應事件
慶幸的是,針對這個問題,咱們有更好的解決方案,即利用冒泡的原理實現的事件委託。
咱們只監聽最外層元素,而後在事件處理函數中根據事件源,即target
屬性,進行不一樣的事件處理,這樣,咱們只須要針對一個元素添加事件處理程序,極大的下降了DOM訪問,而且不須要單獨爲動態添加的元素添加監聽事件了,由於元素的事件會冒泡到最外層,被最外層的事件處理程序截獲,以下:
var ul=document.getElementById('ulList');
ul.onclick=function(e){
var e= e || window.event;
var target = e.target || e.srcElement;
if(target.nodeName.toLowerCase() === "li"){
alert("我是"+e.target);
}
}
複製代碼
從這個例子能夠看出,當用事件委託的時候,徹底不須要遍歷元素的子節點,只須要給父級元素添加事件監聽就行了,以後新添加的子節點也可以一樣的對觸發事件做出適當的響應
- 不是全部事件都是能夠委託的。適合用事件委託的事件有:
click mousedown mouseup keydown keyup keypresss