(2018/3/15 更新)修正示範的 code,原本的根本沒辦法 work @@,很抱歉現在才發現。
DOM 是什麼? DOM element 又是什麼?
addEventListener 是怎麼作用的? useCapture 這個參數是幹嘛的?
e.target 及 e.currentTarget 有什麼不同?
執行 e.stopPropagation() 及 e.preventDefault() 有什麼影響?
如果上面幾個問題也是你的疑問,那請你繼續讀這篇文章,希望能讓你更了解 DOM!
什麼是 DOM 及 DOM 元件
DOM 的全寫是 Document Object Model,是一個 model。
DOM 這個模型表示了 html 頁面上,元件建構以及操縱的邏輯。而且所有 DOM 裡的元件,都有 api 提供了對它作控制的介面,稱為 DOM 元件。
相信這樣的解釋你還是覺得模糊,你可以想成:
用 javascript 選取的 html 元件,都會繼承一套 api,這些 api 就是由 DOM 所制定的,所以稱這個元件為 DOM element。
了解 DOM 所代表的意義了嗎?如果清楚了,請繼續往下看。
DOM 也定義了當事件發生時, 整個 Model 的反應流程。
事件在 DOM 的世界裡,通常代表著有新的使用者行為,包括點擊, 鼠標移動等等。
這些事件是怎麼樣在 DOM 中處理的,請看下面介紹。
DOM 的事件流程
我們先設定情境:有兩個重疊的 div,接收到滑鼠點擊的事件時,會 log 自己的 class 列表。html 如下:
先在頁面上建立兩個相疊的 div, a-class 及 b-class。
/* css */
.b-class {
width: 100px;
height: 100px;
background-color: red;
}
<!-- html -->
<div class="a-class">
<div class="b-class"></div>
</div>
接著讓兩個 div 都監聽 click
事件
var a = document.querySelector('.a-class');
var b = document.querySelector('.b-class');
a.addEventListener('click', function (e) {
console.log(this.classList);
});
b.addEventListener('click', function (e) {
console.log(this.classList);
});
接著在瀏覽器上點擊該 div, console 會依序顯示
["b-class"]
["a-class"]
這個結果告訴我們兩件事:
- 即使 DOM 重疊的情況,所有相關的 listener 都會被執行。
- b 的 listener 會先執行,接著才是 a 的。
要說明這個結果,我們來看一下 W3C DOM Events spec 中的這張 DOM 事件流程圖,它解釋了事件在 html mockup 中派發的過程。
假設現在的事件是點擊,圖中藍色的 <td>
,代表引發事件的 DOM 元件,這張圖就代表:「點擊事件發生在 <td>
身上」。
當事件發生時,會先走紅色的流程,也就是依序通知 Document
-> <html>
-> <body>
-> <table>
-> <tbody>
-> <tr>
-> <td>
這些 DOM 元件:「啊,有點擊事件發生了。」這個到 <td>
為止的階段叫做 capture phase。
接著進行綠色的 bubble phase,就是以相反的方向從 <td>
開始通知流程中的 DOM 有事件發生。在通知完 Document
後,整個事件流程結束。
雖然是先走 capture phase, 但在預設的 addEventListener 方法中, listener 的執行是在 bubble 階段,這說明了為何 b 的 listener 會先執行。(在 capture 階段, a 先被通知,但沒有執行,直到進行到 bubble 階段。)
這個行為是可以設定的,我們來看一下 MDN 怎麼說明 addEventListener 這個方法:
注意第三個參數是 useCapture
,翻成中文就是「是否使用 capture 階段」。
預設沒賦值就是 false, 設為 true 的話,在 capture 階段就會執行該 listener。
現在把 a 的 useCapture
設為 true。
...
a.addEventListener('click', function (e) {
console.log(this.classList);
}, true); // 在 capture phase 就會執行
...
// 點擊該 div
// log 順序變為
// ["a-class"]
// ["b-class"]
因此,當希望某個 html 元件在處理事件擁有優先權時,就可以使用這個參數。
stopPropagation 屬性
在 addEventListener 的 listener 函式中,會有一個 event 物件作為參數
a.addEventListener('click', function(e){
// e 為 event 物件
})
該物件有個方法 stopPropagation
, 執行該方法的話,會中斷事件的派發流程(不論是在 capture 或者 bubble phase)。舉例:
...
a.addEventListener('click', function(e){
e.stopPropagation();
console.log(this.classList);
}, true) // 在 capture phase 就會執行
...
// 點擊該 div
// log ["a-class"]
// b 不會 log
上面設定的情況,事件流程在跑到 capture phase 的 DOM a 時就結束,不會繼續進行後續的階段。
target 及 currentTarget
如果了解了 DOM 是這樣處理事件的,請你想一想 e.target 及 e.currentTarget 有什麼不同?
...
a.addEventListener('click', function(e){
e.stopPropagation();
console.log(e.currentTarget); // e.currentTarget 是指?
console.log(e.target); // e.target 是指?
console.log(this.classList);
})
...
// 點擊 div
// log a DOM element
// log b DOM element
// log ["a-class"]
沒錯, e.currentTarget 是指目前 listener 所配置的 DOM 元件,也就是 a。
而 e.target 則代表觸發本次事件的元件,也就是 b。
DOM 事件的流程,個人認為是身為前端工程師一定要了解的部分。
熟悉之後,你會更加注意 DOM 元件在頁面上的配置順序,以及使用者的行為是怎麼影響頁面上的元素。
題外話 preventDefault
event 物件的另一個方法: preventDefault
,照字面上的意思,代表會阻止預設的行為發生。
預設這個詞換成「原生」應該會更容易理解,原生的行為是指某些 html 內建的元件會有額外的後續動作。舉例:
<form>
<input type="text" name="fname">
<button type="reset">清除</button>
</form>
如果點擊 清除鈕 會重製 input 的內容,如果想要取消或是覆蓋這個行為,就可以用 preventDefault
。i.e
var form = document.querySelector('form');
var resetButton = form.querySelector('[type="reset"]');
resetButton.addEventListener('click', function(e){
e.preventDefault();
// do something else
});
// 點擊 reset 鈕
// 原生的行為不會發生