要小心 JavaScript 的事件代理
要小心 JavaScript 的事件代理
我们知道,如果给 form 里面的 button 元素绑定事件,需要考虑它是否会触发 form 的 submit 行为。除此之外,其它场合给 button 元素绑定事件,你几乎不用担心这个事件会有什么非预期的附加效果,很自然地会这样写事件处理代码:
var button = document.querySelector('button')
button.addEventListener('click', function (e) {
console.log('点击了按钮')
})
你之所以放心这么写,是因为这个 button 元素没有使用事件代理,即没有代理任何子元素的事件。
事件代理的意思是,你要为一个元素绑定事件,但你不是直接把事件绑定到这个元素自己身上,而是绑定到这个元素的父元素上。当子元素的某个事件(比如点击事件)触发时,它的父元素相同的事件也会触发(我们常说的事件冒泡),此时我们说父元素代理了子元素的事件。
举个例子,比如一个 button 元素中包含一个齿轮图标:
<use xlink:href="#gear"></use>
当用户点击齿轮图标,必然要触发 click 事件,但你并不会直接绑定事件到 svg 或 use 元素上,而是绑定到它们的父元素 button 上。即:
document.querySelector('button').addEventListener('click', function (e) {
console.log('点击了按钮')
})
这种情况,我们可以说,button 元素代理了它的所有子元素的 click 事件。
但是,出现这种事件代理的情况时,我们就得小心了。
为了更直观地说明问题,我们把“父”元素上升到顶层的 document 元素:
document.documentElement.addEventListener('click', function (e) {
console.log('我被点击了')
})
只要网页中任意一个位置被点击了,都会触发绑定在 document 元素上的点击事件。 想要知道事件具体是发生在哪个元素上面,可以通过事件对象提供的 target 属性来判断。
document.documentElement.addEventListener('click', function (e) {
console.log(e.target)
})
我们很容易知道事件具体是发生在哪个元素身上的。于是在上面的示例中,如果父元素 document 想在按钮被点击时做点什么事情,我们很自然地会这么写:
document.documentElement.addEventListener('click', function (e) {
if (e.target.tagName === 'BUTTON') {
console.log('按钮被点击了')
}
})
这时问题就出现了,按钮即使被点击了 if 条件也不一定成立,即也不一定会输出“按钮被点击了”。因为用户在按钮上的某个位置点击了,根据用户点击的位置,e.target 可能是下面三种情况:
BUTTON 元素
SVG 元素
USE 元素
实际的情况是这样的:
我们真正的意图是,只要点击是发生在按钮上面,不论是按钮的哪个位置,我们都应视为按钮被点击了。 嗯,简单,我们再改一下,这样写:
document.documentElement.addEventListener('click', function (e) {
if (['BUTTON', 'SVG', 'USE'].includes(e.target.tagName.toUpperCase())) {
// 点击的是按钮
}
})
这样似乎没什么问题,也确实可以达到目的,但看上去总是有些别扭。因为这种情况对于最上层的 document 来说,得知道每个子元素的情况,本来我只需要关心离我最近的 button 元素就可以了。
根据 OOP 对内封装的思想,button 元素内部的事情应该在内部消化掉,其子元素对外不可见,应该只暴露 button 元素本身。依据这个思想和事件冒泡的特点,我们就有了比较好的解决办法:只需要禁止 button 内部元素的事件响应(包括事件冒泡)而只允许 button 元素本身的事件发生就行。有两种方式可以实现这个目的。
一种是使用 CSS 禁止 button 内部元素的事件响应:
button > * {
pointer-events: none;
}
另一种是使用 JS 来阻止 button 内部元素的事件响应(包括事件冒泡):
document.querySelector('button > svg').addEventListener('click', function (e) {
e.stopPropagation()
e.preventDefault()
})
document.querySelector('button').addEventListener('click', function (e) {
console.log(e.target.tagName)
})
这两种方式都能达到我们预期的效果:
综上,针对特定元素进行事件处理时,如果该元素有事件代理的情况,就要小心处理它所代理的子元素。
作者 前端小蜜蜂
原文地址https://www.cnblogs.com/fehoney/p/12962280.html
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Java高级特性之集合
Java高级特性之集合 Java集合框架 一、Java集合框架概述 1、数组与集合的区别: 1)数组长度不可变化而且无法保存具有映射关系的数据;集合类用于保存数量不确定的数据,以及保存具有映射关系的数据。 2)数组元素既可以是基本类型的值,也可以是对象;集合只能保存对象。 2、Java集合类主要由两个根接口Collection和Map派生出来的,Collection派生出了三个子接口:List、Set、Queue(Java5新增的队列),因此Java集合大致也可分成List(有序、可重复集合、可直接根据元素的索引来访问)、Set(无序、不可重复集合、只能根据元素本身来访问)、Queue(队列集合)、Map(存储key-value对的集合,可根据元素的key来访问value)这四种。 二、Java集合常见接口及实现类 1、Collection接口常见方法 2、List集合 实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack。 它们都可以容纳所有类型的对象,包括 null,允许重复,并且都保证元素的存储顺序。(可重复,有顺序) 1)ArrayL...
- 下一篇
像宝石一样的Java原子类
像宝石一样的Java原子类 目录问题:线程之间的协作标准的处理方法:上锁用锁实现计数器和互斥体锁的问题硬件同步原语Compare and swap (CAS)使用CAS实现计数器Lock-free和 wait-free 算法原子变量类细粒度意味着更轻量java.util.concurrent包中的原子变量使用原子变量实现更高的吞吐量总结译者注参考:十五年前,多处理器系统是高度专业化的系统,通常耗资数十万美元(其中大多数具有两到四个处理器)。如今,多处理器系统既便宜又丰富,几乎主流的微处理器都内置了对多处理器的支持,很多能够支持数十或数百个处理器。为了充分利用多处理器系统的性能,通常使用多个线程来构建应用程序。但是,任何一个写并发应用的人都会告诉你,仅仅把工作分散在多个线程中处理不足以充分利用硬件的性能,你必须保证你的线程大部分时间都在工作,而不是在等待工作,或者在等待共享数据上的锁。 问题:线程之间的协作很少有应用可以不依赖线程协作而实现真正的并行化。例如一个线程池,其中的任务通常是彼此独立的被执行,互不干扰。一般会使用一个工作队列来维护这些任务,那么从工作队列中删除任务或向其中添加任...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- CentOS关闭SELinux安全模块
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作