Web開發學習筆記14 — DOM事件、Event Delegation


Posted by Teagan Hsu on 2021-01-01

DOM事件(DOM Event)是什麼?

當網頁瀏覽者透過點擊按鈕、拖移圖片、滑動頁面等等時都會觸發事件的發生。DOM Event定義了這些事件的型態(像是:click、scroll、submit等),讓我們可以透過JavaScript來監聽與處理這些事件。

綁定事件的方法

Inline Events(過時)

將Events綁定在HTML裡,缺點是HTML與JS混用,以及程式碼不簡潔:

<body>
    <input type="button" value="Click me!" onclick="console.log('You Click the button!')">
</body>

The Onclick Property(不太推薦)

透過element.onclick = functionRef觸發事件發生:

//HTML:
<body>
    <input type="button" value="Click me!">
</body>

//JS:
document.querySelector('input').onclick = function() {
    console.log('You Click the button!')
}

addEventListener()(推薦常用)

為什麼推薦使用addEventListener()?
首先參數裡的listener可以是匿名函數,再來與上面兩種綁定事件方法的最大不同在於,addEventListener()可以對同一元素一次綁定好幾個listener。
語法:

target.addEventListener(type, listener [, options]);
target.addEventListener(type, listener [, useCapture]);

參數:

  • type - 各種事件類型
  • listener - 觸發事件後要執行的函式
  • options
    • capture -
      Boolean值,表示listener會在事件捕獲(Capturing)到該EventTarget時觸發
    • once -
      Boolean值,使用once表示listener最多只調用一次。如果為true,則listener在調用後會自動移除此Event。
    • passive
      Boolean值,如果設置為true,則這個事件的handler function永遠不會呼叫event.preventDefault(),此可以改善滾屏的效能。
  • useCapture -
    Boolean值,為true的話則是捕獲事件(Event Capturing),設置為false則為事件冒泡(Event Bubbling)

範例:

//HTML:
<body>
  <input type="button" value="Click me!">
</body>

//JS:
let callback = function() {
    console.log('You Click the button!')
}
document.querySelector('input').addEventListener('click', callback )

Event Capturing and Bubbling(捕獲事件與冒泡事件)

捕獲事件(Event Capturing)

假設我們的EventTarget是li,在捕獲事件的機制下,事件處理器(event handler)會從根元素開始逐一往下觸發。
捕獲事件中事件處理器的觸發順序為:document => body => div => ul => li

冒泡事件(Event Bubbling)

假設我們的EventTarget是li,在冒泡事件的機制下,事件處理器(event handler)會從此事件目標(EventTarget)開始逐一往上觸發。
冒泡事件中事件處理器的觸發順序為:li => ul => div => body => document

Dom Event Flow


圖片來源:W3C
從這張圖來看更能了解事件觸發的順序,假設我們觸發了<td>,首先會從document一路往下直到找到EventTarget(也就是<td>),然後事件處理器再從EventTarget逐一往上觸發事件。
一個簡單的原則:先捕獲再冒泡,大多時候事件處理器都在Bubbling Phase時被觸發

範例:

//HTML:
<body>
    <div>div
        <ul>ul
            <li>li</li>
        </ul>
    </div>

//JS:
let div = document.querySelector('div');
let ul = document.querySelector('ul');
let li = document.querySelector('li');
let divFunc = function() {
    alert('div');
}
let ulFunc = function() {
    alert('ul');
}
let liFunc = function() {
    alert('li');
}
li.addEventListener('click', liFunc)
div.addEventListener('click', divFunc)
ul.addEventListener('click', ulFunc)

結果:clickli,在Bubbling Phase會先觸發li,再按照ul => div的順序逐一往上。

當然我們也可以控制事件處理器在何時被觸發,這要用到addEventListener()的第三個參數useCapture。平時默認為false(也就是Event Bubbling),要讓事件處理器在Capture Phase被觸發,將useCapture加上true即可。
還是用上面的例子,只是將useCapture改為true:

li.addEventListener('click', liFunc, true)
div.addEventListener('click', divFunc, true)
ul.addEventListener('click', ulFunc, true)

結果:clickli,在Capture Phase會先觸發最上層的div,再按照ul => li的順序。

停止事件傳遞

當我們想中斷Capture Phase或Bubbling Phase的傳遞時,可以使用:

  • Event.stopPropagation()
  • Event.stopImmediatePropagation()
    兩者的差別在於,如果今天EventTarget綁定了好幾個listener,想要停止全部與這個EventTarget有關的listener,就必須用Event.stopImmediatePropagation()

範例中增加了li綁定liFunc2()

let liFunc = function(e) {
    alert('li');
    e.stopImmediatePropagation();
}
let divFunc = function() {
    alert('div');
}
let ulFunc = function() {
    alert('ul');
}

let liFunc2 = function() {
    alert('li2');
}
li.addEventListener('click', liFunc)
li.addEventListener('click', liFunc2)
div.addEventListener('click', divFunc)
ul.addEventListener('click', ulFunc)

結果:liFunc()執行完後,跟它同一EventTarget的liFunc2()不會被傳遞。

阻止事件預設行為

這邊要講的語法是Event.preventDefault()。也許有些人容易將Event.stopPropagation()與Event.preventD阻止事件預設行為efault()搞混,Event.preventDefault()的功能在於阻止事件預設行為,所以它並不回停止事件的傳遞。

//HTML:
<body>

    <form>
        <label for="checkBox">Check Box:
    <input id="checkBox"type="checkbox">
</label>
    </form>
    <p></p>
    <script src="阻止事件預設行為.js"></script>
</body>

JS:
let checkBox = document.querySelector('#checkBox');
checkBox.addEventListener('click', (e) => {
    document.querySelector('p').insertAdjacentHTML('beforeend', 'preventDefault()阻止你check這個box<br>')
    e.preventDefault();
})

結果:preventDefault()阻止我們checkbox,但事件的傳遞沒有被中斷。

event.target與event.currentTarget

  • event.Target - 指實際觸發事件的元素
  • event.currentTarget - 指向事件綁定的元素

範例:將事件綁定在ul元素上。

//HTML:
<head>
<style>
ul {padding: 30px;
    margin: 20px;
    border: black 2px solid;}
    </style>
</head>
<body><ul>這裡是ul的位置
        <li>About</li>
        <li>Project</li>
        <li>Contact</li></ul></body>
//JS:
let ul = document.querySelector('ul');
ul.addEventListener('click', (e) => {
    e.target.style.color = 'blue';
    e.currentTarget.style.color = 'red';
})

結果:可以發現點選ul的位置時,li裡的文字都變為紅色,因為這時e.currentTargete.target都指向ul,而當我們再點選li時,被點選的li會變成藍色,這是因為e.target指的是實際觸發事件的元素(也就是包在ul裡的li),而e.currentTarget指向的是事件綁定的元素(也就是ul)。

事件委派(Event Delegation)

剛剛上面有講到event.targetevent.currentTarget的差別,以及冒泡事件的原理,這邊來說說什麼是事件委派(Event Delegation)?

如果想讓按鈕click後,在主控台印出"Click!",我們可能會這樣寫:

<button>Click me!</button>
document.querySelector('button').addEventListener('click', ()=>{
console.log('click')})

但當今天我們有好幾個button要綁定事件處理器,我們可能會這樣寫:

<div id="buttons">
<button>Click me!</button>
<button>Click me!</button>
  <!-- buttons... -->
<button>Click me!</button>
</div>

//用for loop遍歷元素、把callback獨立出來
 let callback = function() {
            console.log('click')
        }
        let buttons = document.querySelectorAll('button');
        for (let button of buttons) {
            button.addEventListener('click', callback);
        }

這樣是行得通的,但我們每loop一次,就有一個button被綁定事件監聽器。這邊有更好、更有效率的寫法,那是用事件委派(Event Delegation)。事件委派(Event Delegation的用法很簡單,就是將事件監聽器附加到按鈕的"父級",並在單擊按鈕時捕獲冒泡事件。
以我們的例子來說,按鈕的父元素是div,所以我們這樣寫:

 let buttons = document.querySelector('#buttons')  //步驟1
        buttons.addEventListener('click', (e) => { //步驟2
            if (e.target.nodeName === 'BUTTON') { //步驟3
                console.log('Click!');
            }
        });
  • 步驟1:
    先選取button的父元素(也就是<div id = "#buttons"></div>
  • 步驟2:
    記得將事件監聽附加到父元素
  • 步驟3:
    使用event.target選擇目標元素(e.target會指向<button>),因為這邊未將button設定className,所以使用e.target.nodeName === 'BUTTON'去設立條件。

靠事件委派(Event Delegation)讓我們只需綁定一個事件監聽器即可!

this

運用this可以簡化我們的程式碼:

//HTML:
 <style>
        input {
            padding: 50px;
            margin: 20px;
            font-size: 30px;
        }
    </style>
</head>

<body>

    <input type="button" value="Click">
    <input type="button" value="Click">
    <input type="button" value="Click">
    <input type="button" value="Click">
    <input type="button" value="Click">

    <script src="DOM-this.js"></script>
</body>

讓changeBackgroundColor()裡的this指向其他的事件觸發元素。

//JS:
let inputs = document.querySelectorAll('input');

let rgbColor = function() {
    let r = Math.floor(Math.random() * 256);
    let g = Math.floor(Math.random() * 256);
    let b = Math.floor(Math.random() * 256);
    return `rgb(${r}, ${g}, ${b})`;
}


let changeBackgroundColor = function() {
    this.style.backgroundColor = rgbColor();
    this.style.color = rgbColor();
}

for (let input of inputs) {
    input.addEventListener('click', changeBackgroundColor);
}

結果:


  1. DOM 的事件傳遞機制:捕獲與冒泡
  2. 介紹 DOM 及事件流程
  3. [JS] Event Capturing and Bubbling
  4. EventTarget.addEventListener() - MDN
  5. e.currentTarget與e.target的區別
  6. Event.stopPropagation() - MDN
  7. Event.preventDefault() - MDN
  8. Event.stopImmediatePropagation()
  9. A Simple Explanation of Event Delegation in JavaScript
    10.Event Delegation — 事件委派介紹 與 觸發委派的回呼函數

#DOM Event #addEventListener() #Bubbling #Event Delegation #capture







Related Posts

前端雜談

前端雜談

簡明程式解題入門 - 陣列篇 III

簡明程式解題入門 - 陣列篇 III

D27_ALG 101-Unit 6

D27_ALG 101-Unit 6


Comments