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

事件


在網頁中,若是想與使用者進行「互動」,必需要經過某種方法知道他都作了什麼。固然,瀏覽器開發者們早已根據 W3C 事件規範[1]實現好了底層的邏輯,咱們只須要經過 Web API 中的 DOM Event[2],經過註冊想監聽的 DOM 元素和事件的事件監聽器(Event Listener)就能夠輕鬆掌握使用者在網頁上的一舉一動。react

事件監聽


咱們能夠在想要監聽事件的 DOM 元素上經過 addEventListener[3] 註冊監聽器。例如:git

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

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

如今的網站有大量的互動,若是經過事件監聽一個一個去寫,除了效能不好,寫起來也很麻煩;這時就體現出「事件代理」的重要性了!瀏覽器

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

時間傳遞


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

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

  • 捕獲階段:由 DOM Tree 的根節點依次向內傳遞,過程當中觸發各別元素的捕獲階段事件監聽。
  • 目標階段:到達事件目標(Event Target),按照註冊順序觸發事件監聽[4]。
  • 冒泡階段:由事件目標依序向外傳遞,過程當中觸發各別元素的冒泡階段事件監聽。
    如圖所示,當使用者觸發一個DOM 元素的事件時,首先會進入捕獲階段(Capture Phase),從根結點逐步向事件目標傳遞;到達目標後則進入目標階段(Target Phase),接着就開始折返,進入向根結點傳遞的冒泡階段(Bubbling Phase)

在使用 addEventListener 註冊事件監聽器時,能夠經過傳遞第三個參數,指定此事件監聽要在什麼階段觸發:ide

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

相關文章
相關標籤/搜索