當(dāng)點擊頁面的某個按鈕時峰搪,單擊事件并不僅僅發(fā)生在按鈕上糜值。換句話說剂桥,在單機按鈕的同時忠烛,也點擊了按鈕的容器元素,甚至也單機了整個頁面权逗。
==事件流描述的是從頁面中接受時間的順序==美尸,IE和Netscape提出了幾乎相反的事件流的概念:
- IE的事件流是事件冒泡流
- Netscape是事件捕獲流
==大多是情況下都是將事件處理程序添加到事件冒泡階段==
事件冒泡
IE的事件流叫做事件冒泡(event bubbling),即事件開始時由最具體的元素(嵌套層次最深的節(jié)點)接受斟薇,然后逐級向上傳播师坎,直至文檔根節(jié)點。
事件冒泡是自內(nèi)而外的去觸發(fā)各個層次綁定的事件
以下面的HTML頁面為例:
<!DOCTYPE html>
<html>
<head>
<title>Event Bubling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>
當(dāng)點擊了<div>
元素堪滨,click
事件會按照如下順序傳播
graph LR
div-->body
body-->html
html-->document
事件從我們單擊的元素開始胯陋,沿著DOM樹向上傳播,在每一級節(jié)點上都會發(fā)生袱箱,直至傳播到document
對象
事件捕獲
Netscape提出的另一種事件流是事件捕獲(event capturing)遏乔,即不太具體的節(jié)點應(yīng)該更早接收到事件,最具體的節(jié)點應(yīng)該最后接收到事件发笔。事件捕獲的用于在于事件到達(dá)預(yù)訂目標(biāo)之前捕獲它盟萨。
仍然以上面的頁面作為例子,在事件捕獲的狀態(tài)下了讨,點擊<div>
元素捻激,click
事件會按照相反的順序傳播:
graph LR
document-->html
html-->body
body-->div
事件捕獲過程中,document
對象最先接收到click
事件前计,然后事件沿DOM樹向下胞谭,直至傳播到事件的實際目標(biāo),即<div>
元素
DOM事件流
==DOM2級事件規(guī)定的事件流包括了三個階段==:
- 事件捕獲階段
- 處于目標(biāo)階段
- 事件冒泡階段
首先發(fā)生的事件捕獲残炮,為截獲事件提供了機會韭赘。然后是實際的目標(biāo)接收到事件,最后一個階段是冒泡階段势就,可以在這個階段對事件作出相應(yīng)泉瞻。
以前面的HTML頁面為例,單機<div>
元素會按照下圖順序觸發(fā)事件
在DOM事件流中苞冯,實際的目標(biāo)<div>
元素在捕獲階段不會接收到事件袖牙,這意味著在捕獲階段事件從document
到html
再到body
后就停止了。下一個階段是“處于目標(biāo)”階段舅锄,事件在<div>
上發(fā)生鞭达。然后冒泡階段發(fā)生,事件有傳播回document
==在事件處理中,事件處于目標(biāo)階段被看成冒泡階段的一部分畴蹭。==
==為了最大限度的兼容坦仍,大多是情況下都是將事件處理程序添加到事件冒泡階段。不是特別需要叨襟,不建議在事件捕獲階段注冊事件處理程序==
DOM2級事件處理程序
DOM2級事件定義了對兩個方法:
-
addEventListener
:添加事件處理程序 -
removeEventListener
:刪除事件處理程序
所有DOM節(jié)點都包括這兩個方法繁扎,它們都接受3個參數(shù),前兩個參數(shù)是要處理的事件名和作為事件處理程序的函數(shù)糊闽。
第三個參數(shù)是一個布爾值梳玫,如果是true
,表示在捕獲階段調(diào)用事件處理程序右犹,如果是false
提澎,表示在冒泡階段調(diào)用事件處理程序。默認(rèn)為false
看一個例子:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>事件冒泡</title>
<style>
#outer {
width: 500px;
height: 300px;
background: darkcyan;
}
#inner {
width: 300px;
height: 150px;
background: chocolate;
}
</style>
</head>
<body>
<div id="outer">Parent
<div id="inner">Child</div>
</div>
</body>
<script>
const outer = document.getElementById('outer'),
inner = document.getElementById('inner');
outer.addEventListener('click', function () {
console.log('parent')
});
inner.addEventListener('click', function () {
console.log('child')
});
</script>
</html>
而點擊child
念链,事件在冒泡階段觸發(fā)盼忌,首先被觸發(fā)的是child
本身,打印了child
掂墓,然后向上層冒泡碴犬,觸發(fā)parent
的事件,打印出parent
將addEventListener
的第三個參數(shù)改變?yōu)?code>true梆暮,則事件觸發(fā)由冒泡階段轉(zhuǎn)為捕獲階段服协,此時,當(dāng)點擊child
啦粹,事件在捕獲階段的傳播從document
開始偿荷,向下傳播到parent
,然后再到child
本身唠椭,所以會先打印parent
跳纳,然后才打印出child
當(dāng)冒泡階段和捕獲階段都會調(diào)用事件處理程序時,事件按DOM事件流的順序執(zhí)行時間處理程序
當(dāng)事件處于目標(biāo)階段時贪嫂,事件調(diào)用順序決定于綁定事件的書寫順序寺庄。
outer.addEventListener('click', function() {
console.log('parent冒泡')
}, false);
outer.addEventListener('click', function() {
console.log('parent捕獲')
}, true);
inner.addEventListener('click', function() {
console.log('child冒泡')
}, false);
inner.addEventListener('click', function() {
console.log('child捕獲')
}, true);
上面的代碼,由于child是處于目標(biāo)階段的事件力崇,執(zhí)行的順序和我們書寫的順序有關(guān)斗塘,因此是'child冒泡'
在前,而'child捕獲'
在后亮靴。而parent
的事件就會按照DOM事件流順序執(zhí)行馍盟,即先'parent捕獲'
后'parent冒泡'
因此,這種情況下的調(diào)用順序是:
graph LR
parent捕獲-->child冒泡
child冒泡-->child捕獲
child捕獲-->parent冒泡
如果我們改動一下書寫順序:
outer.addEventListener('click', function() {
console.log('parent冒泡')
}, false);
outer.addEventListener('click', function() {
console.log('parent捕獲')
}, true);
inner.addEventListener('click', function() {
console.log('child捕獲')
}, true);
inner.addEventListener('click', function() {
console.log('child冒泡')
}, false);
那么調(diào)用順序就變成了:
graph LR
parent捕獲-->child捕獲
child捕獲-->child冒泡
child冒泡-->parent冒泡
事件委托
事件委托就是把一個元素響應(yīng)事件的函數(shù)委托到父層或者更外層元素上茧吊,真正綁定事件的是外層元素贞岭,當(dāng)事件響應(yīng)到需要綁定的元素上時八毯,會通過事件冒泡機制從而觸發(fā)它的外層元素的綁定事件上,然后在外層元素上去執(zhí)行函數(shù)瞄桨。
事件委托的優(yōu)點:
- 提高性能
- 動態(tài)綁定事件(不必再為動態(tài)生成的话速、需要綁定事件的元素逐一綁定事件)
這樣的一個例子:鼠標(biāo)放到li上
,讓對應(yīng)的li
背景變紅:
<body>
<ul id="ul">
<li>item1</li>
<li>item2</li>
<li>item3</li>
</ul>
</body>
<script>
document.querySelector('#ul').addEventListener('mouseover', function (e) {
if(e.target.tagName.toLowerCase() === 'li') {
document.querySelectorAll('#ul li').forEach(v => {
if(v.tagName.toLowerCase() === 'li') {
v.style.backgroundColor = '#fff'
}
});
e.target.style.backgroundColor = 'red'
}
});
// 上面的forEach遍歷也可以通過再添加一個事件處理程序完成
// document.querySelector('#ul').addEventListener('mouseout', function (e) {
// if(e.target.tagName.toLowerCase() === 'li') {
// e.target.style.backgroundColor = '#fff'
// }
// })
</script>
</html>
target
屬性可返回事件的目標(biāo)節(jié)點(觸發(fā)該事件的節(jié)點)芯侥,如生成事件的元素尿孔、文檔或窗口。
這樣避免了在所有的li
上綁定事件筹麸,而且如果是動態(tài)生成的節(jié)點词身,直接在li
上綁定事件就需要再次綁定忙干,而事件委托就不需要。
利用jquery也可以實現(xiàn):
$("ul").on("click","li",function(){
// some code here
})
其中on
方法的第二個參數(shù)是一個jQuery選擇器较坛,用于指定哪些后代元素可以觸發(fā)綁定的事件留晚。如果該參數(shù)為null
或被省略酵紫,則表示當(dāng)前元素自身綁定事件(實際觸發(fā)者也可能是后代元素,只要事件流能到達(dá)當(dāng)前元素即可)错维。
阻止事件冒泡
event.stopPropagation()
可以使用event.stopPropagation()
來阻止事件繼續(xù)傳播奖地,在上面的parent
和child
的例子中,如果將child
綁定的事件改為下面的代碼:
outer.addEventListener('click', function() {
console.log('parent冒泡')
}, false);
inner.addEventListener('click', function() {
console.log('child冒泡');
e.stopPropagation()
}, false);
這樣再次點擊child
赋焕,就不會觸發(fā)parent
的click
事件
e.target
也可以通過e.target
來阻止事件冒泡
document.body.addEventListener('click', e = > {
// 通過e.target判斷阻止冒泡
if (e.target && e.target.matches('a')) {
return;
}
})
return false
JS的原生的事件綁定中参歹,return false
是不能阻止事件冒泡的,它可以用來阻止瀏覽器的默認(rèn)事件隆判,作用于event.preventDefault()
相同
在jQuery綁定的事件中使用return false
犬庇,既阻止默認(rèn)行為又停止冒泡
參考
- 《JavaScript高級程序設(shè)計》第十三章
- 掘金 - JavaScript事件流
- segmentfault - JS中事件冒泡與捕獲
- 知乎 - JavaScript 事件委托詳解