Event Delegation — 事件委派介紹 與 觸發委派的回呼函數
事件委派是一種綁定事件的模式,將多個回呼邏輯綁定在同一個上層節點,它是什麼?我們要怎麼觸發它呢?讓我們先從介紹事件委派開始。
介紹
為什麼會有事件委派的模式呢?
- 過多重複的監聽器 — 10*10的按鈕 掛載一百個重複的click事件
- 掛載、移除事件是有成本的 — removeEventListener 超級麻煩
為什麼可以將事件委派?
- 事件的冒泡機制 — 把子節點們的事件統一處理
- 事件的target屬性 — 辨別事件的位置
題外話:如果想看看更多關於事件機制的討論,可以看這篇《事件 (Event) 的註冊、觸發與傳遞》。
舉例
假設存在一個表單:
<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 之類的)
- 但實際上你監聽器不只一個
看到這發現本篇文章有點掛羊頭賣狗肉的味道,不過有了上述的理解之後我們可以切入正題 —
如何觸發委派的事件回呼?
無論是在測試或是寫網頁套件,我們都有機會接觸到事件委派的程式碼,當然我們都明白要觸發事件有幾個很簡單的心法:
- button之類的節點具有 click 之類的方法 — 自動冒泡
- 透過 WebDriver 模擬使用者點擊 — 自動冒泡
- 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的熱情的話,這篇文章對你或許有幫助,下次在追蹤別人的頁面程式碼時,勢必能理解這個模式存在的必要。