通過前面的DOM事件模型和事件綁定的姿勢兩節的學習,我們對JavaScript中的DOM事件有了一定的基礎。但忽略了有關事件如何被觸發的重要細節。
事件傳播
讓我們從事件傳播開始。事件傳播是事件冒泡和事件捕獲的總稱。先來看一張圖,這張圖在前面的文章中有也出現過,但沒有深入的介紹。先上圖吧!
W3C規範中定義了三個事件階段,依次是捕獲階段、目標階段和冒泡階段。事件對象按照上圖的傳播路徑依次完成這些階段。如果某個階段不支持或事件對象的傳播被終止,那麼該階段就會被跳過。詳細的闡述,我們後面會通過示例來向大家描述,這樣會更易於理解。這裡先來瞭解這三個事件階段的概念:
- 捕獲階段:在事件對象到達事件目標之前,事件對象必須從
window 經過目標的祖先節點傳播到事件目標。這個階段被我們稱之為捕獲階段。在這個階段註冊的事件監聽器在事件到達其目標前必須先處理事件
- 目標階段:事件對象到達其事件目標。這個階段被我們稱為目標階段。一旦事件對象到達事件目標,該階段的事件監聽器就要對它進行處理。如果一個事件對象類型被標誌為不能冒泡。那麼對應的事件對象在到達此階段時就會終止傳播
- 冒泡階段:事件對象以一個與捕獲階段相反的方向從事件目標傳播經過其祖先節點傳播到
window 。這個階段被稱之為冒泡階段。在此階段註冊的事件監聽器會對相應的冒泡事件進行處理
理解概念總是很累的。為了更好的瞭解事件及其工作細節,我們來創建一個簡單的示例,通過示例來幫助我們理解事件的工作細節,比如前面提到的事件傳播。
先來創建示例所需要的HTML結構:
<body id="theBody" class="item">
<div id="one_a" class="item">
<div id="two" class="item">
<div id="three_a" class="item">
<button id="buttonOne" class="item">one</button>
</div>
<div id="three_b" class="item">
<button id="buttonTwo" class="item">two</button>
<button id="buttonThree" class="item">three</button>
</div>
</div>
</div>
<div id="one_b" class="item">
</div>
</body>
正如你所看到的,這裡沒有什麼特別之處。HTML結構很簡單,我們用前面學到的知識,把上面的HTML結構用DOM樹描繪出來,將會像下面這樣:
假設我們點擊了buttonOne 元素。從前面學到的知識中我們知道,這將會觸發一個click 事件。click 事件實際上並不源於你與之交互的元素。因為事件會從你的文檔開始:
從根開始,事件從上一級一級往下,並在觸發事件的元素buttonOne (事件目標)處停止:
如圖所示,click 事件所採用的路徑是直接的,他會通知該路徑中的每個元素。這也意味著,你要監聽body 、one_a 、two 、three_a 上的click 事件,也就會觸發相關的事件處理程序。這是一個很重要的細節。稍後我們再詳細來介紹。
現在,一旦你的事件達到目標,它就不會停止。它會重新追溯它的步驟,回到根源:
就像以前一樣,在事件向上移動時,事件路徑上的每個元素都會被通知它的存在。
其實這兩個過程,前者通常被稱為事件捕獲,而後者被稱為事件冒泡。接下來我們來聊聊事件捕獲和事件冒泡的一些細節。
事件捕獲
有一個細節我們需要注意,在DOM中啟動事件的位置並不重要。因為事件始終從根開始,沿著路徑直到達到目標,然後再重新追溯根源,然後回到根。而其中啟動事件的部分和事件從根向下阻止DOM稱為事件捕獲階段:
在此階段,僅調用捕獲監聽器,也就是addEventListener 的第三個參數為true :
element.addEventListener('click', listener, true)
如果省略此參數,則其默認值為false ,並且監聽器不是捕獲器,而變成冒泡。因此,在此階段期間,僅調用從window 到事件目標父級的路徑上找到的捕獲器。
回到我們的示例中:
Array.from(document.querySelectorAll('.item')).forEach(item => {
var id = item.id;
item.addEventListener('click',function(e){
console.log(`${e.type}: ${id}`)
}, true);
})
點擊不同的元素,可以看到事件捕獲的過程:
具體的Demo,可以點擊這裡,打開瀏覽器開發者工具,你點擊不同的元素時,在console 項中會輸出對應的事件捕獲過程。
事件冒泡
上面看到的是事件傳播的第一階段,即事件捕獲階段。接下來是看另一個階段,即事件冒泡階段:
在事件冒泡階段,只會調用非捕獲者。也就是說,只有addEventListener 的第三個參數的值為false 的事件監聽器。比如上面的示例,把addEventListener 的第三個參數設置為false ,或者不顯式的設置(因為其默認值為false ):
具體的Demo,可以點擊這裡,打開瀏覽器開發者工具,你點擊不同的元素時,在console 項中會輸出對應的事件冒泡過程。
事件中斷
現實中,很多時候我們並不希望目標元素的事件結束之後還去追溯其根源(冒泡)。也就是想在需要的地方可以結束事件的生命。在JavaScript中可以在事件對象上使用stopPropagation 方法:
function handleClick(e) {
e.stopPropagation();
// do something
}
也就是說,stopPropagation 方法可以阻止事件在各個階段中運行。來看一個示例,比如你在three_a 元素有一個click 事件,並且希望阻止事件傳播。比如像下面這樣:
Array.from(document.querySelectorAll('.item')).forEach(item => {
var id = item.id;
item.addEventListener('click',function(e){
console.log(`${e.type}: ${id}`)
}, true);
})
Array.from(document.querySelectorAll('.item')).forEach(item => {
var id = item.id;
item.addEventListener('click',function(e){
console.log(`${e.type}: ${id}`)
}, false);
})
var theElement = document.querySelector("#three_a");
theElement.addEventListener("click", doSomething, true);
function doSomething(e) {
e.stopPropagation();
}
當你點擊buttonOne 元素時,事件的路徑變成下面這樣:
按理說,click 事件捕獲會從window 一級一級向下移動DOM樹,並且會通知到路徑上的每個DOM元素,直到目標元素buttonOne 停止。但上面的代碼,我們在three_a 元素的click 事件的捕獲監聽事件doSomething 時調用了stopPropagation 方法。事件的捕獲到three_a 將會停止。來看一下添加stopPropagation 方法前後的效果:
從上圖我們可以看到,此時你雖然點擊了buttonOne 元素,但程序卻無法捕獲到該元素的上的click 事件。而且事件也不會再冒泡到根元素。
停止即時傳播
正如其名稱的含義,stopImmediatePropagation 會立即停止,甚至阻止了當前監聽器的兄弟姐妹接收事件。
如果有多個相同類型事件的事件監聽函數綁定到同一個元素,當該類型的事件觸發時,它們會按照被添加的順序執行。如果其中某個監聽函數執行了 event.stopImmediatePropagation() 方法,則當前元素剩下的監聽函數將不會被執行。
來看下個簡單的示例:
let buttonOneEle = document.getElementById('buttonOne')
// buttonOne 綁定的第一個監聽函數
buttonOneEle.addEventListener("click", (event) => {
console.log(`第一 ${event.type}: ${event.target.id}`);
}, false);
// buttonOne 綁定的第二個監聽函數
buttonOneEle.addEventListener("click", (event) => {
console.log(`第二 ${event.type}: ${event.target.id}`);
//執行stopImmediatePropagation方法,阻止click事件冒泡,並且阻止p元素上綁定的其他click事件的事件監聽函數的執行
event.stopImmediatePropagation();
}, false);
// 該監聽函數排在上個函數後面,該函數不會被執行
buttonOneEle.addEventListener("click",(event) => {
console.log(`${event.type}: ${event.target.id}`);
}, false);
// buttonOne 元素的click事件沒有向上冒泡,該函數不會被執行
Array.from(document.querySelectorAll('.item')).forEach(item => {
item.addEventListener('click',function(event){
console.log(`${event.type}: ${event.target.id}`)
}, false);
})
在瀏覽器打開Demo,點擊buttonOne 元素或者其他元素時,在console 中輸出的結果像下面這樣:
阻止默認行為
preventDefault 它是事件對象(Event )的一個方法,作用是取消一個目標元素的默認行為。既然是默認行為,那就說元素必須要有默認行為才能被取消,如果元素自身沒有默認行為,調用該方法就會無效。
下例演示了如何使用preventDefault 方法來阻止一個input 元素內非法字符的輸入。
<body>
<p>請輸入一些字母,只允許小寫字母.</p>
<form>
<input type="text" id="my-textbox"/>
</form>
<script type="text/javascript">
function checkName(evt) {
var charCode = evt.charCode;
if (charCode != 0) {
if (charCode < 97 || charCode > 122) {
evt.preventDefault();
alert("只能輸入小寫字母." + "\n"
+ "charCode: " + charCode + "\n"
);
}
}
}
document.getElementById('my-textbox').addEventListener(
'keypress', checkName, false
);
</script>
</body>
另外,在監聽器中調用事件對象的preventDefault 方法,可以避免使用事件取消執行默認操作。你可以查看 event.cancelable 屬性來判斷一個事件的默認動作是否可以被取消. 在cancelable 屬性為false 的事件上調用 preventDefault 方法沒有任何效果.
preventDefault 方法不會阻止該事件的進一步冒泡。 event.stopPropagation 方法才有這樣的功能.
總結
在這篇文章中,我們深入的瞭解了JavaScript中DOM事件傳播機制。JavaScript的DOM事件會經歷捕獲階段、目標階段和冒泡階段三個階段。通過示例介紹事件捕獲和事件冒泡機制,以及如何中斷事件、取消事件和阻止事件冒泡等。
|