DOM 事件是前端开发者习觉得常的东西. 事件的监听和触发使用起来都很是方便, 可是他们的原理是什么呢? 浏览器是怎样处理 event绑定和触发的呢?javascript
让咱们经过实现一个简单的event 处理函数, 来详细了解一下.html
这个相比你们都很清楚了, 有三种注册方式:前端
<button onclick="alert('hello!');">Say Hello!</button>
复制代码
onXXX
属性赋值document.getElementById('elementId').onclick = function() {
console.log('I clicked it!')
}
复制代码
addEventListener()
注册事件 (好处是能注册多个 event handler)document.getElementById('elementId').addEventListener(
'click',
function() {
console.log('I clicked it!')
},
false
)
复制代码
简单的来讲: 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 的传递流程.
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 的源码, 但思路是相同的)
首先让咱们理清浏览器处理 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
}
复制代码
function calculateNodePath(event) {
let target = event.target
let elements = [] // 用于存储从当前节点到html节点的 节点路径
do elements.push(target)
while ((target = target.parentNode))
return elements.reverse() // 节点顺序为: targetElement ==> html
}
复制代码
// 依次触发 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)
}
}
}
复制代码
function executeInPropertyHandler(ev) {
if (!ev.isPropagationStopped) {
ev.target['on' + ev.type].call(ev.target, ev)
}
}
复制代码
// 基本上和 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)
}
}
}
复制代码
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 时, 浏览器的大体处理流程.
咱们知道 event 有 stopPropagation()
和 preventDefault()
两个方法, 他们的做用分别是:
stopPropagation()
stopPropagation()
后, 后续的 handler 将不会被触发.preventDefault()
<a>
标签不进行跳转,<form>
标签点击 submit 后不自动提交表单.当咱们须要对 event handler 执行流进行精细操控时, 这两个方法会很是有用.
addEventListener()
最后一个参数为 false注册 event handler 时, 浏览器默认是注册的 bubble 类型 (即默认状况下注册的 event handler 触发顺序为: 从当前节点到 html 节点)
addEventListener()
的实现是 native codeaddEventListener是由浏览器提供的 api, 并不是 JavaScript 原生 api. 用户触发 event 时, 浏览器会向 message queue
中加入 task, 并经过 Event Loop 执行 task 实现回调的效果.
reference links:
这里是个人博客的 github 地址, 欢迎 star & fork :tada:
邮箱: ssthouse@163.com