瀏覽器 DOM 元素的事件代理指的是什麼

事件


在網頁中,如果想與使用者進行“互動”,必須要通過某種方法知道他都做了什麼。當然,瀏覽器開發者們早已根據 W3C 事件規範[1]實現好了底層的邏輯,我們只需要通過 Web API 中的 DOM Event[2],通過註冊想監聽的 DOM 元素和事件的事件監聽器(Event Listener)就可以輕鬆掌握使用者在網頁上的一舉一動。

事件監聽


我們可以在想要監聽事件的 DOM 元素上通過 addEventListener[3] 註冊監聽器。例如:

document.querySelector('#id').addEventListener('click', clickHandler)

當點擊 #id 元素時會觸發 clickHandler 並傳入一個事件,其內容包含事件傳遞過程中必要的數據,例如目標元素、當前元素、傳遞階段等等。這時我們便可以從中獲取所需要的數據,並針對這些數據做你想做的事。

現在的網站有大量的互動,如果通過事件監聽一個一個去寫,除了效能很差,寫起來也很麻煩;這時就體現出“事件代理”的重要性了!

不過在說到事件代理之前,現需要理解 DOM Tree 上的時間傳遞機制是怎樣的

時間傳遞


可以參考 W3C 所定義的 Event Flow 圖:
瀏覽器 DOM 元素的事件代理指的是什麼

規範中定義了時間傳遞的三個階段:

  • 捕獲階段:由 DOM Tree 的根節點依次向內傳遞,過程中觸發各別元素的捕獲階段事件監聽。
  • 目標階段:到達事件目標(Event Target),按照註冊順序觸發事件監聽[4]。
  • 冒泡階段:由事件目標依序向外傳遞,過程中觸發各別元素的冒泡階段事件監聽。
    如圖所示,當使用者觸發一個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
})

通過簡單的來回傳遞,這樣就能更精準的控制觸發的時機了!

事件代理


現在終於聊到了事件代理。由於事件傳遞的機制,子元素的事件在傳遞過程中勢必會經過它的父元素;而事件代理,顧名思義就是將子元素事件監聽器交由父元素代理。

什麼意思呢?我們直接看個簡單的對照例子:

首先是 HTML 骨架:

<button id="push">push</button>
<button id="pop">pop</button>

<ul id="list"></ul>

沒有事件代理

(function() {
  document.querySelector('#push').addEventListener('click', pushHandler)
  document.querySelector('#pop').addEventListener('click', popHandler)

  const list = document.querySelector('#list')

  function pushHandler() {
    list.appendChild(getNewElem(list.childNodes.length))
  }

  function popHandler() {
    document.querySelectorAll('#list>li')[list.childNodes.length - 1].remove()
  }

  function getNewElem(text) {
    const elem = document.createElement('li')
    elem.innerText = text
    elem.addEventListener('click', eventHandler)
    return elem
  }

  function eventHandler(e) {
    alert(e.target.innerText)
  }
})()

有事件代理

(function() {
  document.querySelector('#push').addEventListener('click', pushHandler)
  document.querySelector('#pop').addEventListener('click', popHandler)

  const list = document.querySelector('#list')

  list.addEventListener('click', listClickHandler)

  function pushHandler() {
    list.appendChild(getNewElem(list.childNodes.length))
  }

  function popHandler() {
    document.querySelectorAll('#list>li')[list.childNodes.length - 1].remove()
  }

  function getNewElem(text) {
    const elem = document.createElement('li')
    elem.innerText = text
    return elem
  }

  function listClickHandler(e){
    if (e.target.tagName === 'LI') alert(e.target.innerText)
  }
})()

差異在於事件監聽的目標元素

在沒有事件代理的版本中每一個 li 上都註冊了事件監聽器,當數量越來越多時瀏覽器也就建立了越來越多的監聽器,無形中對性能有很大的影響;反之在有事件代理的版本中,將事件監聽器註冊在了外層的 ul 上,無論內容有多少,瀏覽器都只需要承擔一組事件監聽器的消耗。

庫和框架中的事件處理


在 DOM 事件處理的這部分,jQuery 和 Vue 都將原生的事件監聽器做了封裝,方便我們快速設定、使用,甚至會自動幫你移除無用的事件監聽。

但是在 React 中,React DOM 上直接註冊的事件監聽器,其實監聽的是 React 額外封裝過的 React DOM Event,並將全部事件代理到 document 上,這與原生事件有很大不同;特別是如果混用 React DOM Even tListener 及原生的 addEventListener,事件監聽器之間的執行順序很有可能會和預期不一致,在寫 React 的時候要特別注意。

有興趣深入研究的話可以在React 源碼[5] 中查找關於事件處理的代碼部分。

Reference

[1]
W3C 事件規範:
https://www.w3.org/TR/uievents/

[2]
DOM Event:
https://developer.mozilla.org/zh-CN/docs/Web/API/Event

[3]
addEventListener:
https://developer.mozilla.org/zh-TW/docs/Web/API/EventTarget/addEventListener

[4]
按照註冊順序觸發事件監聽:
https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener

[5]
React 源碼:
https://github.com/facebook/react/blob/master/packages/react-dom/src/client/ReactDOMClientInjection.js#L26

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章