身为一个前端码农,在开发中遇到凡是需要与用户互动或是需要由用户触发的功能,总是离不开事件处理。
今天聊聊浏览器的 DOM 事件传递机制。
DOM 事件
在浏览器的 Javascript 引擎解析 HTML、SVG 时,会将内容分析成一个个的 DOM (Document Object Model),当用户与 DOM 产生互动时,则是通过 DOM 上注册的事件监听器,去触发某个事件。
例如常见的 onClick
、onTouchStart
,输入框的 onInput
、onChange
、onBlur
等,都是常用的事件类型。
事件监听
例如我们曾经最熟悉的 jQuery,我们会用这样的方式去注册事件监听:
$('#id').on('click', function(){ ... })
但 jQuery 已经成为明日黄花;在现代框架中,Vue 对注册事件监听器提供了一些语法糖,让你写起来很轻松:
<button @click="clickHandler">click me!</button>
React 除了语法糖外,底层还将 DOM 事件再封装一层,并帮你全都代理到 document
上,性能很不错:
<button onClick={clickHandler}>click me!</button>
当然不管是什么框架,底层都等同于通过 Javascript 进行操作:
document.querySelector('#id').addEventListener('click', clickHandler)
事件代理
前面说到 React 会帮你把事件代理到 document 上,这是什么意思呢?
看这个的 简单小例子,点击按钮新增 li
时,会一并注册事件监听:
<!--HTML-->
<button id="push">push</button>
<button id="pop">pop</button>
<ul id="list"></ul>
/*JavaScript*/
(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 pushHandler() {
list.appendChild(getNewElem(list.childNodes.length))
}function getNewElem(text) {
const elem = document.createElement('li')
elem.innerText = text
elem.addEventListener('click', () => alert(text))
return elem
}
如果把事件监听注册在外层的 ul
,并在点击事件触发时判断触发到到的是谁:
function listClickHandler(e){
if (e.target.tagName === 'LI') alert(e.target.innerText)
}
通过事件代理,无论内容有多少,事件监听都只会有一组,效能得到了很大的提升。
移除事件监听
注册事件监听器很方便,但在确定不会再使用监听器时,要记得通过 removeEventListener
将事件监听移除。如果留下了无用的事件监听器,将会造成内存的浪费,对性能有很大的损害。
大家应该注意到了,在前面那个简易的小例子中并没有移除事件监听,而且每创建一个新的子元素,都会同时创建新的函数:
function getNewElem(text) {
const elem = document.createElement('li')
elem.innerText = text
// 在这里创建新的匿名函数
elem.addEventListener('click', () => alert(text)) return elem
}
比较好的写法是把匿名函式抽出来,并在移除子元素时一并移除事件监听器:
function popHandler() {
const elem = document.querySelectorAll('#list>li')[list.childNodes.length - 1]
elem.removeEventListener('click', eventHandler) // 移除事件监听
elem.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)
}
在 Vue 和 React 等主流网页框架中,只要是使用内建的语法注册的事件监听,它们都会自动在无用的时候移除,可以放心使用;如果是自己实现事件监听,务必要记得移除。
捕获与冒泡
跑题太远了,所以到底什么是捕获与冒泡?
浏览器中的事件传递过程分成三个阶段:
- 捕获阶段:由 DOM 树的最外层依序向内,过程中触发个别元素的捕获阶段事件监听。
- 目标阶段:到达事件目标,按照注册顺序触发事件监听。
- 冒泡阶段:由事件目标依序向外,过程中触发个别元素的冒泡阶段事件监听。
这就是刚刚提到的事件代理的机制了;在事件传递过程中,捕获冒泡阶段必然会经过外层元素,因此可以将事件监听注册到外层元素上。
另外,当我们在用 addEventListener
注册事件监听器时,可以传递第三个参数,指定这个事件要在什么阶段触发:
elem.addEventListener('click', eventHandler) // 未指定,预设为冒泡
elem.addEventListener('click', eventHandler, false) // 冒泡
elem.addEventListener('click', eventHandler, true) // 捕获
elem.addEventListener('click', eventHandler, {
capture: true // 是否为捕获。 IE、Edge 不支援。其他属性请参考 MDN
})
如上图所示, 当一个 DOM 事件发生时,会由最外层的 window
开始依次向内传递事件,一直传到我们的事件目标,触发完目标上注册的事件监听,再进入冒泡阶段反向传递;由指定触发的阶段,就能确定执行的顺序了。
本文首发微信公众号:前端先锋
欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章
欢迎继续阅读本专栏其它高赞文章:
- 深入理解Shadow DOM v1
- 一步步教你用 WebVR 实现虚拟现实游戏
- 13个帮你提高开发效率的现代CSS框架
- 快速上手BootstrapVue
- JavaScript引擎是如何工作的?从调用栈到Promise你需要知道的一切
- WebSocket实战:在 Node 和 React 之间进行实时通信
- 关于 Git 的 20 个面试题
- 深入解析 Node.js 的 console.log
- Node.js 究竟是什么?
- 30分钟用Node.js构建一个API服务器
- Javascript的对象拷贝
- 程序员30岁前月薪达不到30K,该何去何从
- 14个最好的 JavaScript 数据可视化库
- 8 个给前端的顶级 VS Code 扩展插件
- Node.js 多线程完全指南
- 把HTML转成PDF的4个方案及实现