1、事件冒泡
要理解事件冒泡,就得先知道事件流。事件流描述的是從頁面接收事件的順序,比如如下的程式碼:
1 2 3 4 5 |
<body> <div> click me! </div> </body> |
如果在body和div內都註冊了click的事件監聽,之後又點選了div區域,是body先響應還是div先響應?有意思的是,當時的瀏覽器開發團隊IE和Netscape提出了差不多完全相反的事件流的概念。IE的事件流是事件冒泡流,而Netscape提出的事件流是事件捕獲流。
IE的事件流叫做事件冒泡,即事件開始時由最具體的元素接收,然後逐級向上傳播到較為不具體的節點(文件)。如上程式碼,點選click事件會這樣傳播:div->body->html->document(雖然我沒寫html元素,但是頁面上預設還是會存在的)
現代的所有瀏覽器都支援事件冒泡,但還是有些細微差別。IE5.5以及更早版本中的事件冒泡會跳過<html>
元素(從body直接跳到document)。IE9、ff、chrome和safari則將事件一直冒泡到window物件。
2、事件捕獲
Netscape團隊則提出另一種事件流-事件捕獲。事件捕獲的思想是不太具體的節點應該更早接收到事件,而最具體的節點應該最後接收到事件,如果仍以上面的程式碼舉例:document->html->body->div。
雖然事件捕獲是Netscape唯一支援的事件流模型,但是IE9、Safari、chrome、opera和ff目前也都支援這種事件流模型。儘管“DOM2級事件”規範要求事件應該從document物件開始傳播,但這些瀏覽器都是從window物件開始捕獲事件的。
因為老版本的瀏覽器不支援事件捕獲,所以我們建議使用事件冒泡。
3、DOM事件流
“DOM2級事件”規定事件流包括三個階段:事件捕獲階段、處於目標階段和事件冒泡階段。還是上面的程式碼作為例子,單擊div元素會按照如下順序觸發事件:document->html->body->div->body->html->document。
在DOM事件流中,實際的目標(div)在捕獲階段不會接收到事件。這意味著在捕獲階段,事件到body就停止了,下一個階段是“處於目標”階段,於是事件在div上發生,並在事件處理中被看成冒泡階段的一部分。然後,冒泡階段發生,事件又傳播迴文件。但是多數支援DOM事件流的瀏覽器都實現了一種特定的行為:即使“DOM2級事件”規範明確要求捕獲階段不會涉及目標事件,但IE9、safari、chrome、ff和opera9.5及更高版本都會在捕獲階段觸發事件物件上的事件,結果就是有兩個機會在目標物件上面操作。(IE9、opera、ff、chrome和Safari都支援DOM事件流,IE8及更早版本不支援DOM事件流)。
4、事件處理程式
響應某個事件的函式就叫做事件處理程式。
DOM0級的事件處理程式很簡單,onclick
就是常用的DOM0級事件處理函式,只會在冒泡階段被處理。
而“DOM2級事件”定義了兩個方法,用於處理指定和刪除事件處理程式的操作:addEventListener()
和removeEventListener()
,所有DOM節點都包含這兩個方法,並且它們都接受3個引數:要處理的事件名、作為事件處理程式的函式和一個布林值。最後這個布林值引數如果是true,表示在捕獲階段呼叫事件處理程式;如果是false,表示在冒泡階段呼叫。DOM2級方法新增事件處理程式的好處是可以新增多個事件處理程式,會按照新增順序被處理(無論是捕獲還是冒泡)。這也是為什麼DOM0級事件相容各種瀏覽器,我們卻還是要使用DOM2的原因之一。
1 2 3 4 5 6 7 8 |
var div = document.getElementById('myDiv'); div.addEventListener('click', function() { console.log(this.id); }, true); div.addEventListener('click', function() { console.log('hello world'); }, true); |
而IE與DOM不同,它有自己的方法:attachEvent()
和detachEvent()
,這兩個方法接受相同的兩個引數:事件處理程式名稱和事件處理程式函式。由於IE8以及更早版本只支援事件冒泡,所以通過attachEvent()新增的事件處理程式都會被新增到冒泡階段(所以不需要第三個引數)。
1 2 3 4 |
var div = document.getElementById('myDiv'); div.attachEvent('onclick', function() { console.log('hello world'); }); |
注意第一個引數是onclick
,而非DOM標準的click
。在IE中使用attachEvent()與使用DOM0級方法的主要區別在於事件處理程式的作用域,在使用DOM0級方法的情況下,事件處理程式會在其所屬元素的作用域內執行,而在使用attachEvent()方法的情況下,事件處理程式在全域性作用域中執行,因此this等於window(這點要特別注意!!!)。attachEvent()也能新增多個事件處理程式,但是事件的執行順序和新增順序相反。
5、跨瀏覽器的事件處理程式
因為瀏覽器之間的差異(其實就是IE大家都懂的),所以需要編寫跨瀏覽器的事件處理程式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
var EventUtil = { addHandler: function(element, type, handler) { if (element.addEventListener) { // DOM2 element.addEventListener(type, handler, false); } else if (element.attachEvent) { // IE element.attachEvent('on' + type, handler); } else { // DOM0 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; } } }; |
6、事件物件
在觸發DOM上的某個事件時,會產生一個事件物件event,這個物件包含著所有與事件有關的資訊。坑爹的是DOM中的事件物件和IE又有不同的玩法。
先來說說DOM中的:
1 2 3 4 5 6 7 8 |
var div = document.getElementById('myDiv'); div.onclick = function(e) { console.log(e.type); }; div.addEventListener('click', function(e) { console.log(e.type); }, false); |
上面程式碼我們應該都不陌生,分別實現了DOM0級和DOM2級的事件物件。
e有很多的屬性和方法,這裡提幾個常用的。target
和currentTarget
,target指的是事件的真正目標,而currentTarget指的是當前的目標,正是利用target我們可以做事件代理。
要阻止特定事件的預設行為,我們可以使用preventDefault()
方法,例如連結的預設行為就是在被單擊時會導航到其href指定的url,如果你想阻止這個預設行為,那麼通過連結的onclick事件處理程式可以取消它:
1 2 3 4 |
var link = document.getElementById('myLink'); link.onclick = function(e) { e.preventDefault(); }; |
只有cancelable
屬性設定為true的事件,才可以使用preventDefault()
來取消其預設行為。
另外,stopPropagation()
方法用於立即停止事件在DOM層中的傳播,即取消進一步的事件捕獲或冒泡。
1 2 3 4 5 6 7 8 9 |
var div = document.getElementById('myDiv'); div.onclick = function(e) { console.log('click!'); e.stopPropagation(); }; document.body.onclick = function(e) { console.log('hello world'); }; |
而IE中的事件物件是這麼用的:
1 2 3 4 5 6 7 8 9 10 |
var div = document.getElementById('myDiv'); div.onclick = function() { var e = window.e; console.log(e.type); }; div.attachEvent('onclick', function(e) { // 也可以通過window.e訪問 console.log(e.type); }); |
IE中的event物件也有很多屬性和方法,比如srcElement
就是和DOM中的target
屬性相同,而returnValue
屬性相當於DOM中的preventDefault()
方法,它們的作用都是取消給定事件的預設行為。只要將該值設定為false,就可以阻止預設行為。相應地,canceBubble
屬性和DOM中的stopPropagation()
方法作用相同,因為IE只支援冒泡,所以它只能取消事件冒泡。
跨瀏覽器的事件物件:
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 |
var EventUtil = { getEvent: function(e) { return e ? e : window.e; }, getTarget: function(e) { return e.target || e.srcElement; }, preventDefault: function(e) { if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; } }, stopPropagation: function(e) { if (e.stopPropagation) { e.stopPropagation() } else { e.cancelBubble = true; } } } |
7、事件委託
有了以上作為基礎,事件委託應該是很簡單了。什麼是事件委託?對“事件處理程式過多”問題的解決方案就是事件委託。事件委託利用了事件冒泡,只指定一個事件處理程式,就可以管理某一型別的所有事件。例如,click事件會冒泡到document層次,也就是說,我們可以為整個頁面指定一個onclick事件處理程式,而不必給每個可單擊的元素分別新增事件處理程式。
舉個經常舉的例子,比如有如下程式碼:
1 2 3 4 5 |
<ul id='myLink'> <li id='a'> apple </li> <li id='b'> banana </li> <li id='c'> orange </li> </ul> |
需要的效果是每點選相應的<li>
選項,alert它裡面的單詞,或許很簡單:
1 2 3 4 5 6 |
var lis = document.getElementsByTagName('li'); for(var i = 0, len = lis.length; i < len; i++) { lis[i].onclick = function() { alert(this.innerHTML); }; } |
但是如上程式碼繫結了三個事件,我們知道每個事件繫結都需要佔用一定的記憶體,更糟糕的是,如果在程式碼執行過程中,動態地又新增了一個li,這時它沒有繫結click的事件,我們還需要手動新增!這時,我們就可以用到事件委託技術:
1 2 3 4 |
var f = document.getElementById('myLink'); f.onclick = function(e) { console.log(e.target.innerHTML); }; |
好吧,就是這麼簡單!