本章從較簡單的主題開始,談談瀏覽器中 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