(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"]

這個結果告訴我們兩件事:

  1. 即使 DOM 重疊的情況,所有相關的 listener 都會被執行。
  2. b 的 listener 會先執行,接著才是 a 的。

要說明這個結果,我們來看一下 W3C DOM Events spec 中的這張 DOM 事件流程圖,它解釋了事件在 html mockup 中派發的過程。

DOM event flow

假設現在的事件是點擊,圖中藍色的 <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 這個方法:
AddEventListener Syntax
注意第三個參數是 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 鈕
// 原生的行為不會發生