Event Delegation — 事件委派介紹 與 觸發委派的回呼函數

事件委派是一種綁定事件的模式,將多個回呼邏輯綁定在同一個上層節點,它是什麼?我們要怎麼觸發它呢?讓我們先從介紹事件委派開始。

realdennis
6 min readMay 25, 2019

介紹

為什麼會有事件委派的模式呢?

  • 過多重複的監聽器 — 10*10的按鈕 掛載一百個重複的click事件
  • 掛載、移除事件是有成本的 — removeEventListener 超級麻煩

為什麼可以將事件委派?

  • 事件的冒泡機制 — 把子節點們的事件統一處理
  • 事件的target屬性 — 辨別事件的位置

題外話:如果想看看更多關於事件機制的討論,可以看這篇《事件 (Event) 的註冊、觸發與傳遞》

連寫文章都能import自己的舊文章

舉例

假設存在一個表單:

<form>
<button id="btn1"> Choice A </button>
<button id="btn2"> Choice B</button>
</form>

有 2 個按鈕都有各自的click事件

const btn1 = document.querySelector('form > button#btn1');
btn1.addEventListener('click', ()=>{
/* do something when btn1 be clicked */
})
const btn2 = document.querySelector('form > button#btn2');
btn2.addEventListener('click', ()=>{
/* do something when btn2 be clicked */
})
...

有10個呢?100個呢?

如果他們都只是把一全域變數count做+1(相同、重複的回呼行為),你會不會覺得自己在搬磚?會。

不過會點開我的這種標題的文章 大概不會是搬磚仔…

事件冒泡 與 target 屬性

  • 事件被觸發後如果沒特別 stopPropagation 將會冒泡上去
  • 事件有兩個特別屬性 target & currentTarget
  • target 告訴你觸發的位置
  • currentTarget 則是你綁定處理器的位置

信手拈來一個 Event Delegation

const form = document.querySelector('form');
form.addEventListener('click',e=>{
switch(e.target.id){
case 'btn1':
/* do something if btn1 click */
break;
case 'btn2':
/* do something if btn2 click */
break;
default:
break;
}
})

恭喜你把程式邏輯放在一個 function 解決掉,透過簡單的switch case去撰寫對應行為或是忽略對應位置的點擊事件。

來做個簡單的比較:

I. 如果回呼行為一樣我把callback獨立出來?

  • 不錯的想法 至少未來很好封裝
  • 不過整個頁面程式的監聽器可能會過多

II. 一個一個 query 出來綁定事件?

  • 心很累
  • query 成本大
  • 未來DOM節點移除後吃exception (耦合)

III. […form.children].forEach 這樣只要一行耶?

  • 想法與 delegation 接近了 (透過高階函數比較 children 之 id 之類的)
  • 但實際上你監聽器不只一個

看到這發現本篇文章有點掛羊頭賣狗肉的味道,不過有了上述的理解之後我們可以切入正題 —

如何觸發委派的事件回呼?

無論是在測試或是寫網頁套件,我們都有機會接觸到事件委派的程式碼,當然我們都明白要觸發事件有幾個很簡單的心法:

  1. button之類的節點具有 click 之類的方法 — 自動冒泡
  2. 透過 WebDriver 模擬使用者點擊 — 自動冒泡
  3. dispatchEvent — 手動叫它冒泡

前兩者當然本篇文章主要介紹第 3 個做法,如何讓它冒泡。

我們都知道可以透過 dispatchEvent 把事件派發進去:

var node = document.querySelector('p#target');
var event = new Event('click');
node.dispatchEvent(event);

但其實在設定事件的建構式的時候可以告訴它要不要冒泡喔!

上述的例子我們可以這麼寫:

var btn1 = document.querySelector('button#btn1');
const bubblesClick = new Event('click',{
bubbles:true
});
btn1.dispatchEvent(bubblesClick);

如果不特別設定的話是 false ,該事件不會冒泡自然就沒有辦法觸發到父節點的事件以及裡頭的switch case了。

為什麼要 dispatchEvent、new Event()?

  • 撰寫網頁套件時
  • 函式庫的自定義事件 — 如跳過廣告的事件,可能是多個條件滿足後才會觸發

如果有一百個button不就要寫一百次?

事件委派都是委派同一個事件名,所以其實上面已經悄悄告訴你一次炸下去的方法:

const bubblesClick = new Event('click',{
bubbles:true
})
const form = document.querySelector('form');// all children
[...form.children]
.forEach(
children=>children.dispatchEvent(bubblesClick)
)
// all descendants
form.querySelectorAll('button')
.forEach(
children=>children.dispatchEvent(bubblesClick)
)

尾聲

你可曾在 JSX 寫過 removeEventListener 呢?

許多時候,在開發當下也許根本不會操作原生事件,比如在 React 中我們操作到的只是 SyntheticEvent

不過,如果你有著撰寫網頁套件、UserScript的熱情的話,這篇文章對你或許有幫助,下次在追蹤別人的頁面程式碼時,勢必能理解這個模式存在的必要。

Photo by Etty Fidele on Unsplash

--

--