JavaScript淺嚐紀錄No.8-監聽(Event Listener)DOM事件並做出反應!

上一篇文章JavaScript淺嚐紀錄No.7-了解一下DOM吧!已經簡單的介紹DOM是甚麼了,有興趣的可以先去參考。
這裡我只會去介紹addEventListener這個事件監聽器,本筆記不會提有關on-event處理器的事情!

事件監聽器(addEventListener)寫法

1
currentTarget.addEventListener(handler,function(){…},true/false)

事件(Event)從哪裡來的?

MDN

The Event interface represents an event which takes place in the DOM.

w3school-JavaScript Events

HTML events are “things” that happen to HTML elements.

DOM事件(DOM Event)指的是某件事情(如點擊'click'、聚焦'focus')發生在了DOM的身上(如某個元素節點)。

事件傳遞(Event Flow)

簡單介紹一下甚麼是事件(Event)傳遞
事件監聽器中的第3個參數決定這個DOM物件是在Capture階段還是Bubbling階段被觸發(如果這個DOM物件本身就是觸發事件的元素,就是在At target階段的話,就一定會被觸發,而不須理會綁定的參數的是Capture還是Bubbling)。
W3C-Graphical representation of an event dispatched in a DOM tree using the DOM event flow

上面圖片中所畫的紅色跟綠色箭頭就是我們說的Event Flow(事件傳遞),如何去判斷現在是在
事件傳遞的哪個階段,可以透過被監聽的DOM物件的這段語法event.eventPhase來查詢,event.eventPhase回傳的值會讓我們知道這個DOM物件是在哪個階段被觸發的,如果回傳1就代表它是在Capture Phase、2代表At target Phase、3代表 Bubbling Phase,這3個詞組合起來就是一個完整的Event Flow(事件傳遞)。

Event Flow(事件傳遞)的3個階段分別代表什麼?

  • Capture Phase:event事件從上(根元素)到下(target元素)傳遞,並且在途中去觸發有綁定相同event事件的元素(第3個參數要設定true),去尋找觸發event事件的元素。
  • At target Phase:當event事件到達觸發event事件的元素時。
  • Bubbling Phase:event事件從下(target元素)到上(根元素),往來時路傳遞回去,並且在途中去觸發有綁定相同event事件的元素(第3個參數要設定false)。

來看幾個範例:
Capturing
請開啟Chrome 開發者工具來檢視event.target

See the Pen capture phase by KUO JOUCHUN (@JOUCHUN) on CodePen.

當點擊藍色Capturing按鈕時,會發現執行的順序是從listlistCapturingbtnCapturing,這是因為我們將這3個元素第3個參數都設定成true,所以click事件會從藍色btnCapturing這個DOM物件最上層開始往下,順序類似bodylistlistCapturingbtnCapturing,我們沒有在body身上綁定事件監聽器,所以當事件傳遞到body時,並不會觸發任何行為,我們有在listlistCapturing身上各綁定了會在Capture Phase時被觸發的click事件(當然它們也都會在At target Phase時也會被觸發),所以當事件傳遞到listlistCapturing時,會依序觸發綁定的事件。

Bubbling
請開啟Chrome 開發者工具來檢視event.target

See the Pen bubbling phase by KUO JOUCHUN (@JOUCHUN) on CodePen.

當點擊紅色Bubbling按鈕時,會發現執行的順序是跟上面的Capturing按鈕是相反的,是`btnBubbling`→`listBubbling`→`list`,這是因為我們將這3個元素第3個參數都設定成`false`,所以`click`事件會從紅色`btnBubbling`這個DOM物件開始往上,順序類似`btnBubbling`→`listBubbling`→`list`→`body`,同樣的我們並沒有在`body`身上綁定事件監聽器,所以當事件傳遞到`body`時,並不會觸發任何行為,我們有在`listBubbling`及`list`身上各綁定了會在Bubbling Phase時被觸發的`click`事件(當然它們也都會在At target Phase時也會被觸發),所以當事件傳遞到`listBubbling`→`list`時,會依序觸發綁定的事件。

到這裡大家對事件傳遞應該都有一點概念了,但這時我卻產生了一個疑問,如果我是這麼綁定事件的話,會發生甚麼事?
我在list第3個參數設定false,而btnCapturinglistCapturing設定為true,再來看看會是甚麼結果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let list = document.querySelector('.list')
let listCapturing = document.querySelector('.list-capturing')
let btnCapturing = document.querySelector('.btn-capturing')
btnCapturing.addEventListener('click', function (e) {
console.log(`btnCapturing event物件`)
console.log(e)
console.log(`currentTarget被監聽的元素(也是this):`)
console.log(this)
console.log(`觸發click事件的元素(e.target):`)
console.log(e.target)
console.log(`btnCapturing的觸發階段:${e.eventPhase}`)
}, true)

listCapturing.addEventListener('click', function (e) {
console.log(`listCapturing event物件`)
console.log(e)
console.log(`currentTarget被監聽的元素(也是this):`)
console.log(this)
console.log(`觸發click事件的元素(e.target):`)
console.log(e.target)
console.log(`listCapturing的觸發階段:${e.eventPhase}`)
}, true)
list.addEventListener('click', function (e) {
console.log(`list event物件`)
console.log(e)
console.log(`currentTarget被監聽的元素(也是this):`)
console.log(this)
console.log(`觸發click事件的元素(e.target):`)
console.log(e.target)
console.log(`list的觸發階段:${e.eventPhase}`)
}, false)

這時候會發現事件傳遞的方向其實一樣是bodylistlistCapturingbtnCapturingbtnCapturinglistCapturinglistbody,同樣的因為body並沒有綁定任何監聽器所以會被略過,而在事件捕獲階段,到達list時,發現list並沒有綁定在當它處於捕獲階段時的事件監聽器,所以會略過它再到listCapturingbtnCapturing去執行事件函式,但這時候的事件傳遞並不會因為已經到了btnCapturing就停止,而是會繼續執行冒泡階段,所以會繼續從btnCapturing開始再繼續往上傳遞事件,但btnCapturinglistCapturing並沒有綁定會在冒泡階段執行的是監聽器,所以被略過,事件被傳遞到了list時,就發現原來你有綁定處於冒泡階段時的事件監聽器喔!那就來執行一下你的事件函式。

到這我的結論就是:如果有一個DOM物件綁定事件監聽器的話,那麼這個事件一定會從上到下,再從下到上傳遞,至於這個event事件經過其他DOM物件時,會不會被觸發取決於事件傳遞到該DOM物件時,它有沒有綁定監聽器;甚麼時候被觸發,取決於綁定的第3個參數是true(Capture階段)還是false(Bubbling階段),如果是true的話,那麼在事件傳遞是Capture階段經過該DOM物件的話,就會被觸發該事件函式,反之如果是false的話,就會在事件傳遞是Bubbling階段時被觸發。

大家可以看看這部影片techsith-Event Bubbling and Capturing in JavaScript

停止預設行為及停止事件傳遞

  • event.preventDefault()
    甚麼是預設行為?在HTML中的<a href="#"></a>的預設行為就是連結別的網頁,雖然我們會透過設定href="#"來取消跳轉調別的網頁,但認真看一下上面的網址其實是會多出一個#,所以不管怎麼設定其實網頁還是會跳轉。而<input type="submit">的預設行為就是將表單的資料提交到伺服器,也因此按下按鈕時會導致頁面刷新。
    但我們在設計網頁時,常常是需要透過這些它們來再目前頁面做一些效果,例如按下按鈕就會出現一個小方塊,但這些預設行為會導致頁面刷新而無法實現這些效果,因此可以透過event.preventDefault()來取消這些預設行為,但是事件傳遞並不會被取消喔。
    1
    2
    3
    listCapturing.addEventListener('click', function (event) {
    event.preventDefault();
    })
  • event.stopPropagation()
    上面已經有介紹過捕獲或冒泡的事件傳遞,而event.stopPropagation()就是來停止這些傳遞行為的,也因此當在點擊btnCapturing或是brnBubbling時,就不會再將事件從上到下、從下到上傳遞,而是只有是在At_Target階段才能被觸發事件
    1
    2
    3
    listCapturing.addEventListener('click', function (event) {
    event.stopPropagation();
    })

註:event是事件發生時,一定會出現的物件,透過這個event物件可以去尋找事件屬性(event.target可以知道目前點擊的DOM物件)或是使用物件內的方法(event.stopPropagation()停止傳遞行為的方法),通常會用evente來表示event物件。

事件(Event)種類

事件種類太多了,如果想要了解可以參考MDN-Event reference,比較常用的就是滑鼠的click事件、表單的submitchange(表單內容改變並且不在focus的狀態)、input(表單內容改變)事件。

事件委派(Event Delegation)

當需要綁定相同事件在多個不同元素,且這些元素擁有共同的祖宗元素時,就可以借助事件具有傳遞的特性(主要是指Bubbling Event),將該事件綁定在該祖宗元素上,而無須在子元素上一一綁定事件,而這樣的行為就是**事件委派(Event Delegation)**。
為什麼不建議將事件一個一個綁定呢?
這是因為設定一個函式,就會占用一份記憶體空間,所以使用事件委派的方式,可以減少記憶體空間的使用。
另外在不需要事件監聽的狀況下,可以統一移除,也不需要一個一個去移除事件監聽器了。

當我們使用事件委派(Event Delegation)的方式來綁定監聽器,除了借助事件傳遞的特性之外,還借助了事件擁有的屬性event.targetevent.target可以幫助我們知道現在觸發事件的目標元素是哪個,也因此我們可以透過操作這個目標元素來達成我們的目的。

請開啟Chrome 開發者工具來檢視event.target

See the Pen Event Delegation by KUO JOUCHUN (@JOUCHUN) on CodePen.

參考:
JavaScript 事件對記憶體和效能的影響
Event Delegation — 事件委派介紹 與 觸發委派的回呼函數
JavaScript 面試:事件傳遞機制和事件委託 Event Propagation & Event Delegation - 彭彭直播