身为一个前端码农,在开发中遇到但凡须要与用户互动或是须要由用户触发的性能,总是离不开事件处理。

明天聊聊浏览器的 DOM 事件传递机制。

DOM 事件

在浏览器的 Javascript 引擎解析 HTML、SVG 时,会将内容分析成一个个的 DOM (Document Object Model),当用户与 DOM 产生互动时,则是通过 DOM 上注册的事件监听器,去触发某个事件。

例如常见的 onClickonTouchStart,输入框的 onInputonChangeonBlur 等,都是罕用的事件类型。

事件监听

例如咱们已经最相熟的 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 等支流网页框架中,只有是应用内建的语法注册的事件监听,它们都会主动在无用的时候移除,能够放心使用;如果是本人实现事件监听,务必要记得移除。

捕捉与冒泡

跑题太远了,所以到底什么是捕捉与冒泡?

依据 W3C 所定义的 Event Flow:

浏览器中的事件传递过程分成三个阶段:

  • 捕捉阶段:由 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个计划及实现

  • 更多文章...