本章從較簡單的主題開始,談談瀏覽器中 DOM 元素的事件代理(Event Delegation)。
事件#
網頁要與使用者「互動」,必須透過某些方法捕捉使用者的行為。瀏覽器依據 W3C 事件規範實作好底層邏輯,開發者只需透過 Web API 中的 DOM Event,針對想監聽的 DOM 元素與事件註冊事件監聽器(Event Listener),便能掌握使用者在網頁上的一舉一動。
事件監聽#
開發者可以在想要監聽事件的 DOM 元素上,透過 addEventListener 註冊監聽器。例如:
document.querySelector("#id").addEventListener("click", clickHandler);使用者點擊 #id 元素時觸發 clickHandler,並傳入事件物件,內容包含目標元素、當前元素、傳遞階段等資訊。開發者可從中取得所需資料。
現代網站的使用者互動繁多,逐個註冊事件監聽既傷效能也難維護。「事件代理」應運而生。
談事件代理前,需要先理解 DOM Tree 的事件傳遞機制。
事件傳遞#
W3C 規範中定義了事件傳遞的三個階段:
- 捕獲階段:由 DOM Tree 的根節點依序向內傳遞,過程中觸發各別元素的捕獲階段事件監聽。
- 目標階段:到達事件目標(Event Target),依照註冊順序觸發事件監聽。
- 冒泡階段:由事件目標依序向外傳遞,過程中觸發各別元素的冒泡階段事件監聽。
當使用者觸發一個 DOM 元素的事件時,首先會進入「捕獲階段(Capture Phase)」,由根結點逐步向事件目標傳遞;到達目標後則進入「目標階段(Target Phase)」,接著就開始折返,進入向根結點傳遞的「冒泡階段(Bubbling Phase)」。
在開發者使用 addEventListener 註冊事件監聽器時,可以透過傳遞第三個參數,指定此事件監聽要在什麼階段觸發:
elem.addEventListener("click", eventHandler); // 未指定,預設為冒泡
elem.addEventListener("click", eventHandler, false); // 冒泡
elem.addEventListener("click", eventHandler, true); // 捕獲
elem.addEventListener("click", eventHandler, {
capture: true, // 是否為捕獲。IE、Edge 不支援。其他物件屬性請參考 MDN
});透過簡單的來回傳遞,這樣開發者就能更精準的控制觸發的時機。
事件代理#
由於事件傳遞的機制,子元素的事件在傳遞過程中勢必會經過它的父元素;而事件代理,顧名思義就是將子元素事件監聽器交由父元素代理。
什麼意思呢?差異就只在事件監聽的目標元素:
- 無事件代理:事件監聽器註冊在每一個
li上,當數量越來越多,瀏覽器也就建立了越來越多的監聽器,無形中對效能有很大的傷害。 - 有事件代理:將事件監聽器註冊在外層的
ul上,無論內容有多少,瀏覽器都只需要負擔一組事件監聽器的消耗。
延伸閱讀#
現在軟體開發中,使用套件是稀鬆平常的;在 DOM 事件處理的這部分,jQuery 與 Vue 都將原生的事件監聽器做了包裝,方便開發者快速設定、取用,甚至會自動幫你移除無用的事件監聽。
但在 React 中,React DOM 上直接註冊的事件監聽器,其實監聽的是 React 額外封裝過的 React DOM Event,並將全部事件代理到 document 上,與原生事件有蠻大的不同;特別是在執行順序上,若有混用 React DOM Event Listener 及原生的 addEventListener,事件監聽器之間的執行順序很有可能會不如預期,寫 React 的朋友要特別注意。
結語#
前端工程師活用互動式設計讓網站更吸引人的同時,也要善用事件代理,以更優雅、高效的方式處理互動事件。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10219793