天天看點

前端雜談: DOM event 原理前端雜談: DOM event 原理

前端雜談: DOM event 原理

DOM 事件是前端開發者習以為常的東西. 事件的監聽和觸發使用起來都非常友善, 但是他們的原理是什麼呢? 浏覽器是怎樣處理 event綁定和觸發的呢?

讓我們通過實作一個簡單的event 處理函數, 來詳細了解一下.

首先, 如何注冊 event ?

這個相比大家都很清楚了, 有三種注冊方式:

  1. html 标簽中注冊
<button onclick="alert('hello!');">Say Hello!</button>           
  1. 給 DOM 節點的

    onXXX

    屬性指派
document.getElementById('elementId').onclick = function() {
  console.log('I clicked it!')
}           
  1. 使用

    addEventListener()

    注冊事件 (好處是能注冊多個 event handler)
document.getElementById('elementId').addEventListener(
  'click',
  function() {
    console.log('I clicked it!')
  },
  false
)           

event 在 DOM 節點間是如何傳遞的呢 ?

簡單的來說: event 的傳遞是 先自頂向下, 再自下而上

完整的來說: event 的傳遞分為兩個階段: capture 階段 和 bubble 階段

讓我們來看一個具體的例子:

<html>
  <head> </head>

  <body>
    <div id="parentDiv">
      <a id="childButton" href="https://github.com"> click me! </a>
    </div>
  </body>
</html>           

當我們點選上面這段 html 代碼中的 a 标簽時. 浏覽器會首先計算出從 a 标簽到 html 标簽的節點路徑 (即:

html => body => div => a

).

然後進入 capture 階段: 依次觸發注冊在

html => body => div => a

上的 capture 類型的 click event handler.

到達 a 節點後. 進入 bubble 階段. 依次出發

a => div => body => html

上注冊的 bubble 類型的 click event handler.

最後當 bubble 階段到達 html 節點後, 會出發浏覽器的預設行為(對于該例的 a 标簽來說, 就是跳轉到指定的網頁.)

從下圖我們可以更直覺的看到 event 的傳遞流程.

前端雜談: DOM event 原理前端雜談: DOM event 原理

那麼, 這樣的 event 傳遞流是如何實作的呢?

讓我們來看看

addEventListener

的代碼實作:

HTMLNode.prototype.addEventListener = function(eventName, handler, phase) {
  if (!this.__handlers) this.handlers = {}
  if (!this.__handlers[eventName]) {
    this.__handlers[eventName] = {
      capture: [],
      bubble: []
    }
  }
  this.__handlers[eventName][phase ? 'capture' : 'bubble'].push(handler)
}           

上面的代碼非常直覺, addEventListener 會根據 eventName 和 phase 将 handler 儲存在

__handler

數組中, 其中 capture 類型的 handler 和 bubble 類型的 handler 分開儲存.

接下來到了本文的核心部分: event 是如何觸發 handler 的 ?

為了便于了解, 這裡我們嘗試實作一個簡單版本的 event 出發函數

handler()

(這并不是浏覽器處理 event 的源碼, 但思路是相同的)

首先讓我們理清浏覽器處理 event 的流程步驟:

  1. 建立 event 對象, 初始化需要的資料
  2. 計算觸發 event 事件的 DOM 節點到 html 節點的節點路徑 (DOM path)
  3. 觸發 capture 類型的 handlers
  4. 觸發綁定在 onXXX 屬性上的 handler
  5. 觸發 bubble 類型的 handlers
  6. 觸發該 DOM 節點的浏覽器預設行為
1. 建立 event 對象, 初始化需要的資料
function initEvent(targetNode) {
  let ev = new Event()
  ev.target = targetNode // ev.target 是目前使用者真正出發的節點
  ;(ev.isPropagationStopped = false), // 是否停止event的傳播
    (ev.isDefaultPrevented = false) // 是否阻止浏覽器預設的行為

  ev.stopPropagation = function() {
    this.isPropagationStopped = true
  }
  ev.preventDefault = function() {
    this.isDefaultPrevented = true
  }
  return ev
}           
2. 計算觸發 event 事件的 DOM 節點到 html 節點的節點路徑
function calculateNodePath(event) {
  let target = event.target
  let elements = [] // 用于存儲從目前節點到html節點的 節點路徑
  do elements.push(target)
  while ((target = target.parentNode))
  return elements.reverse() // 節點順序為: targetElement ==> html
}           
3. 觸發 capture 類型的 handlers
// 依次觸發 capture類型的handlers, 順序為: html ==> targetElement
function executeCaptureHandlers(elements, ev) {
  for (var i = 0; i < elements.length; i++) {
    if (ev.isPropagationStopped) break

    var curElement = elements[i]
    var handlers =
      (currentElement.__handlers &&
        currentElement.__handlers[ev.type] &&
        currentElement.__handlers[ev.type]['capture']) ||
      []
    ev.currentTarget = curElement
    for (var h = 0; h < handlers.length; h++) {
      handlers[h].call(currentElement, ev)
    }
  }
}           
4. 觸發綁定在 onXXX 屬性上的 handler
function executeInPropertyHandler(ev) {
  if (!ev.isPropagationStopped) {
    ev.target['on' + ev.type].call(ev.target, ev)
  }
}           
5. 觸發 bubble 類型的 handlers
// 基本上和 capture 階段處理方式相同
// 唯一的差別是 handlers 是逆向周遊的: targetElement ==> html

function executeBubbleHandlers(elements, ev) {
  elements.reverse()
  for (let i = 0; i < elements.length; i++) {
    if (isPropagationStopped) {
      break
    }
    var handlers =
      (currentElement.__handlers &&
        currentElement.__handlers[ev.type] &&
        currentElement.__handelrs[ev.type]['bubble']) ||
      []
    ev.currentTarget = currentElement
    for (var h = 0; h < handlers.length; h++) {
      handlers[h].call(currentElement, ev)
    }
  }
}           
6. 觸發該 DOM 節點的浏覽器預設行為
function executeNodeDefaultHehavior(ev) {
  if (!isDefaultPrevented) {
    // 對于 a 标簽, 預設行為就是跳轉連結
    if (ev.type === 'click' && ev.tagName.toLowerCase() === 'a') {
      window.location = ev.target.href
    }
    // 對于其他标簽, 浏覽器會有其他的預設行為
  }
}           
讓我們看看完整的調用邏輯:
// 1.建立event對象, 初始化需要的資料
let event = initEvent(currentNode)

function handleEvent(event) {
  // 2.計算觸發 event事件的DOM節點到html節點的**節點路徑
  let elements = calculateNodePath(event)
  // 3.觸發capture類型的handlers
  executeCaptureHandlers(elements, event)
  // 4.觸發綁定在 onXXX 屬性上的 handler
  executeInPropertyHandler(event)
  // 5.觸發bubble類型的handlers
  executeBubbleHandlers(elements, event)
  // 6.觸發該DOM節點的浏覽器預設行為
  executeNodeDefaultHehavior(event)
}           

以上就是當使用者出發 DOM event 時, 浏覽器的大緻處理流程.

propagation && defaultBehavior

我們知道 event 有

stopPropagation()

preventDefault()

兩個方法, 他們的作用分别是:

stopPropagation()

  • 停止 event 的傳播, 從上面代碼的可以看出, 調用

    stopPropagation()

    後, 後續的 handler 将不會被觸發.

preventDefault()

  • 不觸發浏覽器的預設行為. 如:

    <a>

    标簽不進行跳轉,

    <form>

    标簽點選 submit 後不自動送出表單.

當我們需要對 event handler 執行流進行精細操控時, 這兩個方法會非常有用.

一些補充~

預設

addEventListener()

最後一個參數為 false

注冊 event handler 時, 浏覽器預設是注冊的 bubble 類型 (即預設情況下注冊的 event handler 觸發順序為: 從目前節點到 html 節點)

addEventListener()

的實作是 native code

addEventListener是由浏覽器提供的 api, 并非 JavaScript 原生 api. 使用者觸發 event 時, 浏覽器會向

message queue

中加入 task, 并通過 Event Loop 執行 task 實作回調的效果.

reference links: https://www.bitovi.com/blog/a-crash-course-in-how-dom-events-work https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events

想了解更多 前端 / D3.js / 資料可視化 ?

這裡是我的部落格的 github 位址, 歡迎 star & fork :tada:

D3-blog

如果覺得本文不錯的話, 不妨點選下面的連結關注一下 : )

github 首頁 知乎專欄 掘金

想直接聯系我 ?

郵箱: [email protected]

繼續閱讀