js 事件流的事件冒泡和事件捕獲與阻止事件傳播
為了方便引入事件流的概念,我們先來說說什麽是事件。
事件就是用戶或瀏覽器自身執行的某種動作。換句話說,我們在瀏覽網頁或者 APP 時,通常會在設備上產生很多交互性的操作,例如點擊、選擇、滾動屏幕、鍵盤輸入等等,這些交互性操作就是事件。
因為 DOM 結構是一個樹型結構,所以當一個 HTML 元素產生一個事件時,該事件就會在元素結點與根節點之間按特定的順序傳播,路徑所經過的節點都會收到該事件,這個事件傳播的過程就被稱為 事件流。
但是,當一個 HTML 元素產生一個事件時,是該元素最外層的父元素先收到事件並執行,還是該元素本身先收到事件並執行?
關於這個問題,IE 和 Netscape 開發團隊在開發初期
事件冒泡
IE的事件流叫做事件冒泡(event bubbling)。冒泡,顧名思義,事件像個水中的氣泡一樣一直往上冒,直到頂端。即事件開始時由具體的元素(文檔中嵌套層次最深的那個節點)接收,然後逐級向上傳播到較為不具體的節點(文檔)。以下面的 HTML 頁面為例:
<!DOCTYPE html> <html> <head> <title> Example </title> </head> <body> <div class="one"></div> </body> </html>
如果你單擊了頁面中的<div>
元素,那麽這個 click 事件會按照如下順序傳播:
<div>
<body>
<html>
- document
也就是說,click 事件首先在<div>
元素上發生,而這個元素就是我們單擊的元素。然後,click 事件沿 DOM 樹向上傳播,在每一級節點上都會發生,直至傳播到 document 對象。下圖展示了事件 冒泡的過程。
事件捕獲
Netscape 團隊提出的另一種事件流叫做事件捕獲(event capturing)。事件捕獲的思想是不太具體的節點應該更早接收到事件,而具體的節點應該後接收到事件。事件捕獲的用意在於在事件到達預定目標之前捕獲它。如果仍以前面的 HTML頁面作為演示事件捕獲的例子,那麽單擊<div>
- document
<html>
<body>
<div>
在事件捕獲過程中,document 對象首先接收到 click 事件,然後事件沿DOM樹依次向下,一直 傳播到事件的實際目標,即<div>
元素。下圖展現了事件捕獲的過程。
DOM 事件流
後來,ECMAScript 標準重新將事件流規範為三個階段,分別為事件捕獲階段,處於目標階段,事件冒泡階段。首先發生的是事件捕獲,為截取事件提供了機會。然後是實際的目標接受到事件。最後一個階段是冒泡階段,可以在這個階段對事件做出響應。以前面的 HTML 頁面為例。點擊<div>
元素會按照下圖所示順序觸發事件。
雖然大部分的瀏覽器都遵循著標準,但是在IE瀏覽器中,事件流卻是非標準的。在IE中事件流只有兩個階段:處於目標階段,冒泡階段。如下圖所示:
還有另外一個需要註意的地方,標準的事件流中所有的事件都要經過捕捉階段和目標階段,但是有些事件會跳過冒泡階段。例如,讓元素獲得輸入焦點的 focus 事件以及失去輸入焦點的 blur 事件就都不會冒泡。
阻止事件流
為什麽要阻止事件流
事件就是用戶或瀏覽器自身執行的某種動作。諸如 點擊、加載 和鼠標滑過。而響應某個事件的函數就叫做事件處理函數(又叫事件監聽函數、事件句柄)。
因為事件流的存在,當某個元素的父元素或者子元素與當前元素擁有相同的事件處理函數時一個問題就暴露了出來。
為了更好的說明這個問題,我們把前面的那個簡單的 HTML 拿來重新改造一下。
<!DOCTYPE html>
<html>
<head>
<title> Example </title>
<style>
.one {
width: 100px;
height: 100px;
background-color: #00F;
cursor: pointer;
}
</style>
</head>
<body>
<div class="one"></div>
<script>
const html = document.querySelector('html');
const body = document.body;
const div = document.querySelector('.one');
document.onclick = Event => {
alert("我是:document");
}
html.onclick = Event => {
alert("我是:html");
}
body.onclick = Event => {
alert("我是:body");
}
div.onclick = Event => {
alert("我是:div");
}
</script>
</body>
</html>
我們期望的是當點擊 div 時彈出 “我是:div”,然而...
這就是事件流附帶的一個弊端,在事件冒泡階段, <div>
的 click 事件向上傳播到根節點,除了觸發自己的事件處理函數之外,還順帶觸發了上級元素上的 click 事件處理函數。
也就是說,當一個元素與父元素或祖元素擁有相同的事件,該元素的事件處理函數被觸發時,父元素和祖元素相同的事件也會被觸發。
如何阻止事件流
換句話說,為了避免頁面交互操作出現異常,當一個元素設置一個事件後,它的子元素或者父元素就不能設置同樣的事件。很顯然,這樣的結果並不是我們想要的。難道就沒有什麽解決的方法麽?答案是肯定的:有。
W3C 中有一個 stopPropagation()
事件方法。該方法將停止事件的傳播,阻止它被分派到其他 Document 節點。但是這是一個 DOM 2 級方法,IE 不出意外的不支持,IE 的方法是 e.cancelBubble=true
。
我們把上邊示例的 javascript 代碼拿過來改一下,看看效果如何。
<script>
const html = document.querySelector('html');
const body = document.body;
const div = document.querySelector('.one');
html.onclick = Event => {
window.event ? window.event.cancelBubble = true : e.stopPropagation();
alert("我是:html");
}
body.onclick = Event => {
window.event ? window.event.cancelBubble = true : e.stopPropagation();
alert("我是:body");
}
div.onclick = Event => {
window.event ? window.event.cancelBubble = true : e.stopPropagation();
alert("我是:div");
}
</script>
不錯,這才是我們想要的效果。
參考
- JavaScript高級程序設計(第3版)尼古拉斯 · 澤拉斯 著
- W3C--TML DOM Event 對象:http://www.w3school.com.cn/jsref/dom_obj_event.asp
- 淺談前端和移動端的事件機制:https://juejin.im/post/59ede91ef265da43143fde87#heading-1
js 事件流的事件冒泡和事件捕獲與阻止事件傳播