首先我們思考一個很有意思的事情:一張紙上畫了兩個同心圓,當我們把手指放到圓心上時,手指指向的不是一個圓,而是紙上的兩個圓,同理之,當我們單擊網頁上的一個div塊的時候(程式碼片段一),單擊事件會僅僅作用在這個div上面嗎? 在瀏覽器發展到第四代時,IE和Netscape的開發團隊都遇到這個問題,他們都一致認為,除了單擊div塊,我們也單擊了body、 html、甚至是整個document,但不幸的是兩個團隊針對事件流模型產生了兩個完全相反的概念。
程式碼片段一:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件流</title>
</head>
<body>
<div id="box">Click me</div>
</body>
</html>
1. 事件冒泡(推薦)
IE的事件流稱為事件冒泡。
即:事件由最具體的元素接收(div),逐級向上傳播到不具體的節點(document)。
當我們點選程式碼片段一中id為box的div塊時,單擊事件會按照如下順序傳播:
div ——> body——> html ——> document
如上圖所示,click首先在div元素上發生,然後沿著Dom樹向上傳播,每一級節點都會發生直至傳播到document物件。
測試程式碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件流</title>
</head>
<style type="text/css">
#box1{
width: 300px;
height: 300px;
background-color: red;
}
#box2{
width: 200px;
height: 200px;
background-color: yellow;
}
#box3{
height: 100px;
width: 100px;
background-color: green;
}
</style>
<body>
<div id="box1">
<div id="box2">
<div id="box3"></div>
</div>
</div>
</body>
<script type="text/javascript">
document.getElementById('box1').onclick = function () {
console.log('box1 click')
}
document.getElementById('box2').onclick = function () {
console.log('box2 click')
}
document.getElementById('box3').onclick = function () {
console.log('box3 click')
}
</script>
</html>
測試效果:
note: 幾乎現代所有的瀏覽器都支援事件冒泡,不過有一些細微的差別
IE5.5 和 IE5.5 - 版本的事件冒泡會跳過html元素(body 直接到 document)
IE9、Firefox、Chrome、Safari則一直冒泡到window物件。
2、事件捕獲
Netscape提出的事件流模型稱為事件捕獲。
即:事件從最不具體的節點開始接收(document),傳遞至最具體的節點<div>,和IE的冒泡剛好相反, 事件捕獲的本意是當事件到達預定目標前捕獲它。當我們點選程式碼片段一中id為box的div塊時,單擊事件會按照如下順序傳播:
document——> html ——> body ——> div
note: 雖然事件捕獲是Netscape唯一支援的事件流模型,但IE9、Firefox、Chrome、Safari目前也都支援這種事件模型,由於老版本的瀏覽器並不支援,所以我們應該儘量使用事件冒泡,有特殊需求的時候再考慮事件捕獲。
3、DOM2級事件流
為了能夠相容上述兩種事件模型,又提出了一個DOM2級事件模型,它規定了事件流包含三個階段:
事件捕獲階段:為事件捕獲提供機會;
處於目標階段:事件的目標接收到事件(但並不會做出響應);
事件冒泡階段:事件響應階段;
在DOM2級事件流中,但我們點選程式碼片段一中的div,在事件捕獲階段從document ->html ->body就停止了(div元素在這個階段並不會接收到點選事件)。緊接著,事件在div上發生,並把事件真正的處理看成是冒泡階段的一部分,然後,冒泡階段發生,事件又回傳到document。
測試程式碼:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DOM2級事件流</title>
</head>
<style type="text/css">
#box1{
width: 300px;
height: 300px;
background-color: red;
}
#box2{
width: 200px;
height: 200px;
background-color: yellow;
}
#box3{
height: 100px;
width: 100px;
background-color: green;
}
</style>
<body>
<div id="box1">
<div id="box2">
<div id="box3"></div>
</div>
</div>
</body>
<script type="text/javascript">
var box1 = document.getElementById('box1');
var box2 = document.getElementById('box2');
var box3 = document.getElementById('box3');
// true表示在捕獲階段處理事件、false表示在冒泡階段處理
box1.addEventListener('click',function () {
console.log('事件捕獲階段觸發box1點選事件');
}, true);
box1.addEventListener('click',function () {
console.log('事件冒泡階段觸發box1點選事件');
}, false);
box2.addEventListener('click',function () {
console.log('事件捕獲階段觸發box2點選事件');
}, true);
box2.addEventListener('click',function () {
console.log('事件冒泡階段觸發box2點選事件');
}, false)
box3.addEventListener('click',function () {
console.log('事件捕獲階段觸發box3點選事件');
}, true);
box3.addEventListener('click',function () {
console.log('事件冒泡階段觸發box3點選事件');
}, false)
</script>
</html>
測試結果
4、事件流的典型應用——事件代理
傳統的事件處理中,需要為每個元素新增事件處理器。js事件代理則是一種簡單有效的技巧,透過它可以把事件處理器新增到一個父級元素上,從而避免把事件處理器新增到多個子級元素上。
事件代理的原理用到的就是事件冒泡和目標元素,把事件處理器新增到父元素,等待子元素事件冒泡,並且父元素能夠透過target(IE為srcElement)判斷是哪個子元素,從而做相應處理, 下面舉例說明:
傳統的事件會為每個dom新增事件,程式碼如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>傳統的事件繫結</title>
</head>
<body>
<ul id="color-list">
<li>red</li>
<li>orange</li>
<li>yellow</li>
<li>green</li>
<li>blue</li>
<li>indigo</li>
<li>purple</li>
</ul>
</body>
<script>
(function() {
var colorList = document.getElementById("color-list");
var colors = colorList.getElementsByTagName("li");
for (var i = 0; i < colors.length; i++) {
colors[i].addEventListener('click', showColor, false);
};
function showColor(e) {
e = e || window.event;
var targetElement = e.target || e.srcElement;
console.log(targetElement.innerHTML);
}
})();
</script>
</script>
</html>
事件代理的處理方式如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>傳統的事件繫結</title>
</head>
<body>
<ul id="color-list">
<li>red</li>
<li>orange</li>
<li>yellow</li>
<li>green</li>
<li>blue</li>
<li>indigo</li>
<li>purple</li>
</ul>
<script>
(function() {
var colorList = document.getElementById("color-list");
colorList.addEventListener('click', showColor, false);
function showColor(e) {
e = e || window.event;
var targetElement = e.target || e.srcElement;
if (targetElement.nodeName.toLowerCase() === "li") {
alert(targetElement.innerHTML);
}
}
})();
</script>
</body>
</html>
使用事件代理的好處:
將多個事件處理器減少到一個,因為事件處理器要駐留記憶體,這樣就提高了效能。想象如果有一個100行的表格,對比傳統的為每個單元格繫結事件處理器的方式和事件代理(即table上新增一個事件處理器),不難得出結論,事件代理確實避免了一些潛在的風險,提高了效能。
DOM更新無需重新繫結事件處理器,因為事件代理對不同子元素可採用不同處理方法。如果新增其他子元素(a,span,div等),直接修改事件代理的事件處理函式即可,不需要重新繫結處理器,不需要再次迴圈遍歷。