前端雜談: 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 的傳遞流程.

FP1SMT.png

那麼, 這樣的 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]

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