要理解事件冒泡,就得先知道事件流。事件流描述的是從頁面接收事件的順序,比如如下的代碼:
<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對象。
Netscape團隊則提出另一種事件流-事件捕獲。事件捕獲的思想是不太具體的節點應該更早接收到事件,而最具體的節點應該最後接收到事件,如果仍以上面的代碼舉例:document->html->body->div。
雖然事件捕獲是Netscape唯一支持的事件流模型,但是IE9、Safari、chrome、opera和ff目前也都支持這種事件流模型。盡管“DOM2級事件”規范要求事件應該從document對象開始傳播,但這些浏覽器都是從window對象開始捕獲事件的。
因為老版本的浏覽器不支持事件捕獲,所以我們建議使用事件冒泡。
“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事件流)。
響應某個事件的函數就叫做事件處理程序。
DOM0級的事件處理程序很簡單,onclick
就是常用的DOM0級事件處理函數,只會在冒泡階段被處理。
而“DOM2級事件”定義了兩個方法,用於處理指定和刪除事件處理程序的操作:addEventListener()
和removeEventListener()
,所有DOM節點都包含這兩個方法,並且它們都接受3個參數:要處理的事件名、作為事件處理程序的函數和一個布爾值。最後這個布爾值參數如果是true,表示在捕獲階段調用事件處理程序;如果是false,表示在冒泡階段調用。DOM2級方法添加事件處理程序的好處是可以添加多個事件處理程序,會按照添加順序被處理(無論是捕獲還是冒泡)。這也是為什麼DOM0級事件兼容各種浏覽器,我們卻還是要使用DOM2的原因之一。
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()添加的事件處理程序都會被添加到冒泡階段(所以不需要第三個參數)。
var div = document.getElementById('myDiv');
div.attachEvent('onclick', function() {
console.log('hello world');
});
注意第一個參數是onclick
,而非DOM標准的click
。在IE中使用attachEvent()與使用DOM0級方法的主要區別在於事件處理程序的作用域,在使用DOM0級方法的情況下,事件處理程序會在其所屬元素的作用域內運行,而在使用attachEvent()方法的情況下,事件處理程序在全局作用域中運行,因此this等於window(這點要特別注意!!!)。attachEvent()也能添加多個事件處理程序,但是事件的執行順序和添加順序相反。
因為浏覽器之間的差異(其實就是IE大家都懂的),所以需要編寫跨浏覽器的事件處理程序。
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;
}
}
};
在觸發DOM上的某個事件時,會產生一個事件對象event,這個對象包含著所有與事件有關的信息。坑爹的是DOM中的事件對象和IE又有不同的玩法。
先來說說DOM中的:
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事件處理程序可以取消它:
var link = document.getElementById('myLink');
link.onclick = function(e) {
e.preventDefault();
};
只有cancelable
屬性設置為true的事件,才可以使用preventDefault()
來取消其默認行為。
另外,stopPropagation()
方法用於立即停止事件在DOM層中的傳播,即取消進一步的事件捕獲或冒泡。
var div = document.getElementById('myDiv');
div.onclick = function(e) {
console.log('click!');
e.stopPropagation();
};
document.body.onclick = function(e) {
console.log('hello world');
};
而IE中的事件對象是這麼用的:
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只支持冒泡,所以它只能取消事件冒泡。
跨浏覽器的事件對象:
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;
}
}
}
有了以上作為基礎,事件委托應該是很簡單了。什麼是事件委托?對“事件處理程序過多”問題的解決方案就是事件委托。事件委托利用了事件冒泡,只指定一個事件處理程序,就可以管理某一類型的所有事件。例如,click事件會冒泡到document層次,也就是說,我們可以為整個頁面指定一個onclick事件處理程序,而不必給每個可單擊的元素分別添加事件處理程序。
舉個經常舉的例子,比如有如下代碼:
<ul id='myLink'>
<li id='a'> apple </li>
<li id='b'> banana </li>
<li id='c'> orange </li>
</ul>
需要的效果是每點擊相應的<li>
選項,alert它裡面的單詞,或許很簡單:
var lis = document.getElementsByTagName('li');
for(var i = 0, len = lis.length; i < len; i++) {
lis[i].onclick = function() {
alert(this.innerHTML);
};
}
但是如上代碼綁定了三個事件,我們知道每個事件綁定都需要占用一定的內存,更糟糕的是,如果在代碼執行過程中,動態地又添加了一個li,這時它沒有綁定click的事件,我們還需要手動添加!這時,我們就可以用到事件委托技術:
var f = document.getElementById('myLink');
f.onclick = function(e) {
console.log(e.target.innerHTML);
};
好吧,就是這麼簡單!
JavaScript高級程序設計(第3版)高清完整PDF中文+英文+源碼 http://www.linuxidc.com/Linux/2014-09/107426.htm