身为一个前端小混混,在开发中遇到凡是须要与用户互动或是须要由用户触发的功能,老是离不开事件处理。前端
今天聊聊浏览器的 DOM 事件传递机制。浏览器
在浏览器的 Javascript 引擎解析 HTML、SVG 时,会将内容分析成一个个的 DOM (Document Object Model),当用户与 DOM 产生互动时,则是经过 DOM 上注册的事件监听器,去触发某个事件。app
例如常见的 onClick、onTouchStart,输入框的 onInput、onChange、onBlur 等,都是经常使用的事件类型。框架
例如咱们曾经最熟悉的 jQuery,咱们会用这样的方式去注册事件监听:ide
$('#id').on('click', function(){ ... })
但 jQuery 已经成为明日黄花;在现代框架中,Vue 对注册事件监听器提供了一些语法糖,让你写起来很轻松:函数
<button @click="clickHandler">click me!</button>
React 除了语法糖外,底层还将 DOM 事件再封装一层,并帮你全都代理到 document 上,性能很不错:性能
<button onClick={clickHandler}>click me!</button>
固然无论是什么框架,底层都等同于经过 Javascript 进行操做:3d
document.querySelector('#id').addEventListener('click', clickHandler)
前面说到 React 会帮你把事件代理到 document 上,这是什么意思呢?代理
看这个的 简单小例子,点击按钮新增 li 时,会一并注册事件监听:code
<!--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 等主流网页框架中,只要是使用内建的语法注册的事件监听,它们都会自动在无用的时候移除,能够放心使用;若是是本身实现事件监听,务必要记得移除。
跑题太远了,因此到底什么是捕获与冒泡?
根据 W3C 所定义的 Event Flow:
DOM Event 框架
浏览器中的事件传递过程分红三个阶段:
另外,当咱们在用 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 开始依次向内传递事件,一直传到咱们的事件目标,触发完目标上注册的事件监听,再进入冒泡阶段反向传递;由指定触发的阶段,就能肯定执行的顺序了。