DOM 事件是 JS 中比較重要的一部分知識,所謂事件,簡單理解就是用戶對浏覽器進行的一個操作。事件在 Web 前端領域有很重要的地位,很多重要的知識點都與事件有關,所以學好 JS 事件可以讓我們在JS的學習道路中更進一步。
1、事件流
事件流描述的是從頁面中接受事件的順序。但是 IE 和 Netscape 開發團隊提出了兩個截然相反的事件流概念,IE 的事件流是事件冒泡流,而 Netscape 的事件流是事件捕獲流。
(1)、事件冒泡
事件冒泡,即事件最開始由最具體的元素(文檔中嵌套層次最深的那個節點)接收,然後逐級向上傳播至最不具體的節點(document)。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>事件流</title> </head> <body> <div> <input type="button" value="按鈕"> </div> </body> </html>
上面的實例,在點擊按鈕之後,click 事件會按照如下的順序逐級傳播:
input -> div -> body -> html -> document
所有現代的浏覽器都支持事件冒泡,但是在具體的實現上又有一些差別:
IE5.5 及更早版本中的事件冒泡會跳過 html 元素(從 body 直接跳到 document)。
IE9、Firefox、Chrome 和 Safari 則將事件一直冒泡到 window 對象。
(2)、事件捕獲
事件捕獲的思想是不太具體的 DOM 節點應該更早接收到事件,而最具體的節點最後接收到事件。
按照事件捕獲的思想,上面的實例,click 事件將會以如下的順序逐級傳播:
document -> html -> body -> div -> input
事件捕獲和事件冒泡的傳播順序正好是相反的,雖然事件捕獲是 Netscape 唯一支持的事件流模型,但 IE9、Safari、Chrome、Opera 和 Firefox 目前也都支持這種事件流模型。
(3)、DOM 事件流
"DOM2級事件" 規定的事件流包括三個階段:事件捕獲階段、處於目標階段和事件冒泡階段。即同時支持冒泡和捕獲,在任何事件發生時,先從頂層開始進行事件捕獲,直到事件觸發到達了最具體的元素,然後,再從最具體的元素向上進行事件冒泡,直到到達 document 。
在 DOM 事件流中,實際的目標在捕獲階段不會接收事件。就是說在捕獲階段,事件從 document 到 html 再到 body 後就停止了。下一個階段是“處於目標”階段,於是事件在 div 中發生,並在事件處理中被看成是冒泡階段的一部分。然後,冒泡階段發生。IE8 及更早的版本不支持 DOM 事件流,浏覽器在捕獲階段觸發事件對象上的事件,結果就是有兩個機會在目標對象上面操作事件。
2、事件處理程序
(1)、HTML 事件處理程序
HTML 事件處理程序就是直接把事件添加在 HTML 結構中。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>事件</title> </head> <body> <input type="button" value="按鈕" onclick="alert('Hello')"> </body> </html>
也可以調用在頁面中其他地方定義的腳本:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>事件</title> </head> <body> <input type="button" value="按鈕" onclick="show()"> <script> function show(){ alert('Hello'); } </script> </body> </html>
事件處理程序中的代碼在執行時,有權訪問全局作用域中的任何代碼。
這樣使用會創建一個封裝著的元素屬性值的函數。這個函數有一個局部變量 event ,也就是事件對象:
<input type="button" value="按鈕" onclick="alert(event.type)">
上面的代碼返回:click
<input type="button" value="按鈕" onclick="alert(event.target)">
上面的代碼返回:input 元素
其中,this 值等於事件的目標元素,如:
<input type="button" value="按鈕" onclick="alert(this.value)">
上面的代碼返回:按鈕
所謂的事件對象就是 event,在觸發 DOM 上的事件時都會產生一個事件對象。
type:用於獲取事件類型。target:用於獲取事件目標。
而在 IE8 及更早版本獲取事件目標要使用 srcElement 屬性代替。
所有現代浏覽器引用事件對象,都可以直接使用 event 引用,而在 IE8 及之前版本的浏覽器則需要使用 window.event 引用。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>事件</title> </head> <body> <input type="button" value="按鈕" onclick="show()" > <script> function show(ev){ ev = event || window.event; var ele = ev.target || ev.srcElement; alert(ele); } </script> </body> </html>
HTML事件處理程序的缺點:HTML 代碼和 JS 代碼緊密的耦合在一起,如果需要對事件做出更改,那麼 JS 代碼和 HTML 代碼都需要更改。所以應該使用 JS 指定的事件處理程序。
(2)、JS 傳統事件處理程序
JS 中非常傳統的方式,就是在一個元素上直接綁定方法,即先獲取元素,再以屬性方式添加事件。
該方式也叫做 DOM0級 事件處理程序。element.onclick = function(e){}
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>事件</title> </head> <body> <input type="button" value="按鈕" id="btn"> <script> var oBtn = document.getElementById('btn'); oBtn.onclick = function (){ alert('Hello'); } </script> </body> </html>
把一個函數賦值給一個事件處理程序的屬性,這是用的比較多的方式,非常簡單而且穩定,可適應不同的浏覽器,但是這樣的事件綁定只會在事件冒泡中運行,捕獲不行,且一個事件每次只能綁定一個事件函數。
使用該方式指定的事件處理程序被認為是元素的方法,因此,這時候的事件處理程序是在元素的作用域中運行的,也就是 this 指向當前元素:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>事件</title> </head> <body> <input type="button" value="按鈕" id="btn"> <script> var oBtn = document.getElementById('btn'); oBtn.onclick = function (){ alert(this.type); } </script> </body> </html>
上面的代碼返回:button
DOM0級 在刪除事件時,直接讓事件等於空。如:oBtn.onclick=null;
(3)、W3C 事件處理程序
W3C 中推薦使用 addEventListener() 函數進行事件綁定,使用 removeEventListener() 函數刪除事件。這種方式也叫做 DOM2級 事件處理程序,它們都接收三個參數:要處理的事件名、作為事件處理程序的函數和一個布爾值。
通常事件監聽都是添加在冒泡階段,所以添加事件的布爾值為 false。element.addEventListener('click', function (){...}, false)
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>事件</title> </head> <body> <input type="button" value="按鈕" id="btn"> <script> var oBtn = document.getElementById('btn'); oBtn.addEventListener('click', function (){ alert('Hello'); }, false); </script> </body> </html>
該方式和前邊傳統方式綁定事件的效果是一樣的,這種方式綁定同時支持冒泡和捕獲,addEventListener() 函數最後的參數表達了事件處理的階段:false(冒泡),true(捕獲)。這種方式最重要的好處就是對同一元素的同一個類型事件做綁定不會覆蓋,比如上面代碼中,再給 oBtn 綁定一個 click 事件,那麼兩次綁定的事件都會被執行,按照代碼的順序依次執行。
通過 addEventListener() 函數添加的事件只能通過 removeEventListener() 函數刪除,在刪除事件時,必須傳入與添加事件相同的參數。所以,用 addEventListener() 添加的匿名函數將無法移除。
DOM2級 方式和DOM0級 方式都可以給一個元素添加多個事件,添加的多個事件依次按順序執行。
大多數情況下,都是將事件處理程序添加到事件流的冒泡階段,這樣可以最大限度地兼容各種浏覽器。
(4)、IE 中的事件處理程序
在 IE 浏覽器下不支持 addEventListener() 函數,只能在 IE9 以上(包括IE9)可以使用,IE 浏覽器相應的要使用 attachEvent() 函數代替。
刪除事件則使用 detachEvent() 函數。這兩個方法接收相同的兩個參數:事件處理程序名稱與事件處理函數。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>事件</title> </head> <body> <input type="button" value="按鈕" id="btn"> <script> var oBtn = document.getElementById('btn'); oBtn.attachEvent('onclick', function (){ alert('Hello'); }); </script> </body> </html>
attachEvent() 函數支持事件捕獲的冒泡階段,同時它不會覆蓋事件的綁定。如果在 Firefox、Chrome 等其他內核的浏覽器中使用這個函數去綁定事件,浏覽器會報錯。
如果需要做跨浏覽器的事件處理程序,則需要做兼容性處理,即判斷是否支持 attachEvent 方法,如果支持就使用該方法,如果不支持則使用 addEventListener() 方法。
這裡需要注意: attachEvent() 的第一個參數是“onclick”,而不是 addEventListener()方法中的“click"。
3、事件流阻止
事件流阻止,這裡阻止的是它的繼續傳播以及有可能產生的默認動作。
在一些情況下我們需要阻止事件流的傳播,也就是阻止事件的向上冒泡。
某些事件的對象是可以取消的,這也意味著可以阻止默認動作的發生。
W3C 中定義了兩個方法:stopPropagation() 和 preventDefault() 用於阻止事件流。
event.stopPropagation() 方法用於阻止事件冒泡。
event.preventDefault() 方法用於阻止事件的默認行為。
阻止事件的默認行為在做移動端 app 時,用的比較多,比如 a 鏈接的跳轉,不跳轉新頁面,而是跳轉不同的位置。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>事件</title> </head> <body> <div id="box"> <a href="http://www.baidu.com" id="go">跳轉</a> </div> <script> var oDiv = document.getElementById('box'); var oA = document.getElementById('go'); oDiv.onclick = function (){ alert('我是a鏈接的父容器'); } oA.onclick = function (ev){ ev.stopPropagation(); ev.preventDefault(); } </script> </body> </html>
上面的例子中,點擊 a 鏈接後有一個默認的跳轉行為,並且浏覽器會認為你點了 a 鏈接,也就點了其父容器 div 元素。阻止了事件冒泡和默認行為之後,再點擊跳轉,就不會有任何響應了。
stopPropagation() 和 preventDefault() 這兩個方法都可以阻止事件,前者是取消事件流的繼續冒泡,但是 IE8 及以前的版本不支持該方法,而後者是告訴浏覽器不要執行與事件相關聯的默認動作,比如 submit 類型的按鈕點擊會提交。如果直接使用 return false 則表示終止處理函數。
在 IE8 及之前版本的浏覽器中沒有定義阻止事件流的方法,而是使用屬性,cancelBubble 屬性值為 true 時取消事件冒泡。returnValue 屬性值為 false 時阻止事件的默認行為。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>事件</title> </head> <body> <div id="box"> <a href="http://www.baidu.com" id="go">跳轉</a> </div> <script> var oDiv = document.getElementById('box'); var oA = document.getElementById('go'); oDiv.onclick = function (){ alert('我是a鏈接的父容器'); } oA.onclick = function (ev){ ev = event || window.event; if(ev.stopPropagation){ ev.stopPropagation(); } else{ ev.cancelBubble = true; } if(ev.preventDefault){ ev.preventDefault(); } else{ ev.returnValue = false; } } </script> </body> </html>
上面的代碼,是兼容性寫法,不管是現代的浏覽器還是 IE8 及更早版本的浏覽器都可以使用。