疯狂的技术宅

以前出于工作目的,编写和翻译了大量的技术文章,以前端为主,删掉了过时的、毫无营养的内容,留下的都是精华。


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于本站

  • 回到主站

  • 搜索

捕获冒泡?难道浏览器是鱼吗?

时间: 2020-10-26 分类: 前端技术   字数: 1329 字 阅读: 3分钟
标签: #浏览器# #冒泡# #事件传递#

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

今天聊聊浏览器的 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 等主流网页框架中,只要是使用内建的语法注册的事件监听,它们都会自动在无用的时候移除,可以放心使用;如果是自己实现事件监听,务必要记得移除。

捕获与冒泡

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

根据 W3C 所定义的 Event Flow :

DOM Event 框架

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

  • 捕获阶段:由 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 开始依次向内传递事件,一直传到我们的事件目标,触发完目标上注册的事件监听,再进入冒泡阶段反向传递;由指定触发的阶段,就能确定执行的顺序了。

标签: #浏览器# #冒泡# #事件传递#

标题:捕获冒泡?难道浏览器是鱼吗?

链接:https://fe-tech.viewnode.com/post/202010/26/

作者:疯狂的技术宅

声明: 本博客文章除特别声明外,均采用 CC BY-NC-ND 4.0 国际许可协议( 知识共享署名-非商业性使用-禁止演绎 4.0),转载请注明出处!

通过 POST 获取数据需要注意的问题
CSS 选择器是如何运作的?
  • 文章目录
  • 站点概览
疯狂的技术宅

疯狂的技术宅

退休程序员,硬件发烧友,人工智能爱好者。写写代码喝喝茶,晒晒太阳带带娃。

457 日志
8 分类
583 标签
GitHub
友情链接
  • viewnode
  • mofish
标签云
  • Javascript 172
  • Node.Js 62
  • Vue 36
  • Typescript 28
  • 实战项目 28
  • 面试 21
  • React 20
  • Css 17
  • 面试题 16
  • 教程 13
  • Promise 12
  • Chrome 9
  • Debug 9
  • 调试 9
  • 资源 9
  • Deno 8
  • Dom 8
  • 杂谈 8
  • 正则表达式 8
  • 测试 8
  • DOM 事件
  • 事件监听
  • 事件代理
  • 移除事件监听
  • 捕获与冒泡
© 2018 - 2022 疯狂的技术宅 All Rights Reserved
Powered by - Hugo v0.99.0 / Theme by - NexT
Storage by 俺的服务器 / 冀ICP备2022010157号
0%