前端杂谈: DOM event 原理

DOM 事件是前端开发者习觉得常的东西. 事件的监听和触发使用起来都很是方便, 可是他们的原理是什么呢? 浏览器是怎样处理 event绑定触发的呢?javascript

让咱们经过实现一个简单的event 处理函数, 来详细了解一下.html

首先, 如何注册 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 的传递是 先自顶向下, 再自下而上java

完整的来讲: event 的传递分为两个阶段: capture 阶段bubble 阶段git

让咱们来看一个具体的例子:github

<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).api

而后进入 capture 阶段: 依次触发注册在html => body => div => a上的 capture 类型的 click event handler.数组

到达 a 节点后. 进入 bubble 阶段. 依次出发 a => div => body => html上注册的 bubble 类型的 click event handler.浏览器

最后当 bubble 阶段到达 html 节点后, 会出发浏览器的默认行为(对于该例的 a 标签来讲, 就是跳转到指定的网页.)dom

从下图咱们能够更直观的看到 event 的传递流程.

image

那么, 这样的 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:

www.bitovi.com/blog/a-cras…

developer.mozilla.org/en-US/docs/…

想了解更多 前端 / D3.js / 数据可视化 ?

这里是个人博客的 github 地址, 欢迎 star & fork :tada:

D3-blog

若是以为本文不错的话, 不妨点击下面的连接关注一下 : )

github 主页

知乎专栏

掘金

想直接联系我 ?

邮箱: ssthouse@163.com

欢迎关注个人公众号:

相关文章
相关标签/搜索