弱弱地冒一个泡
经常听到冒泡捕获,那到底是什么东西呢?
事件模型
首先就要介绍一下 W3C DOM 事件模型
事件总体上分为三个阶段
- 捕获阶段,事件从父到子向下传递
- 目标阶段,事件传递到达发生事件的目标点
- 冒泡阶段,事件从子到父向上传递
不论是什么事件,没有特殊设置的情况下,都会按顺序经历这三个阶段
除非该事件被取消冒泡,才会在取消冒泡之后停止传递
前两个阶段不能停止传递
通过 on 赋值可以为元素绑定监听器,此时监听器一定是在事件冒泡阶段发生,且后赋值的监听器会覆盖之前的监听器
例如 button.onclick=()=>{}
通过 addEventListener(eventName, listener, useCapture, priority, useWeakReference) 可以设置监听器是在事件的冒泡阶段还是捕获阶段发生,该方式可以为同一个元素的同一个事件设置多个监听
useCapture 默认值是 false,表示监听器在事件冒泡阶段发生,反之当为 true 时即表示在捕获阶段发生
priority 默认值是 0,表示所有同级的监听器按注册顺序进行。可以为个别监听器设置更高的优先级,优先级高的会在该阶段先执行,同优先级的依然按注册顺序进行
useWeakReference 默认值是 false,表示设置监听器为强引用,使得监听器不被垃圾回收,反之 true 则是允许回收
当要取消监听器、或要重新设定优先级时,可以使用 removeEventListener() 来取消监听器,之后再重新设定
每当事件触发一个监听器的时候,都会向监听器内传入两个默认参数
一个是 this,表示事件现在所处的元素
一个是 event,包含事件的完整信息
target 与 currentTarget
通过在监听器中打印 event,可以发现两件事
- target != currentTarget
- 如果先保存 event,事件结束后再打印,则会发现 target 变成了 null
对于第一点,target 是事件发生的最小元素,也就是唯一有目标阶段的元素
currentTarget 则是事件现在所处的位置,是传入监听器的 this
对于第二点,则涉及到事件消亡的知识,篇幅过大,此处不介绍了
target 元素 事件先后
刚才说到 target 元素是唯一有目标阶段的元素,那么什么是目标阶段?
实际上,对于 target 元素来说,目标阶段就是捕获冒泡连续发生,不像事件传递路径上别的元素是分开发生的
- 哦哦,那我懂了,所以对 target 元素来说,监听器也是按照设置好的捕获冒泡顺序执行的吧
- 不!
此时监听器不再区分捕获和冒泡,统一按照设置顺序发生
取消冒泡
那有时候我想到此为止,不想打扰父级元素,怎么办呢?
可以在需要中断的元素的冒泡阶段的监听器中,执行
1 | e.propagetion() |
此时就可以阻止事件继续冒泡
默认动作
既然冒泡可以阻止,那默认的事件能不能阻止呢?比如我现在有个 a 标签,想做单页面应用的 tab 页跳转,要是按照默认的,可就跳到新页面去了
答案是可以!
在事件中执行
1 | e.preventDefault() |
就可以阻止默认动作的发生了!
禁用滚动
奇怪,说好的阻止默认动作,怎么我在滚动条上不能 prevent,你骗我!
等下等下,你真的找准是什么东西产生了滚动条了吗?
有时候你看着是你的元素产生了滚动条,但说不定是 body 产生的!
找准了之后,我们来禁用滚动条吧——毕竟滚动条其实只是 CSS 产生的东西
禁用滚动一般分两种:
- 看不见滚动条
- 看得见滚动条
对于第一种,其实只需要设置 overflow: hidden 就可以实现了
对于第二种,比较复杂一些
首先能看见滚动条,说明 overflow 至少是 auto,甚至是 scroll
这时候通过划拉滚动条,或者鼠标滚轮,甚至是触屏上的手指,都可以让页面滚动
这种时候就要分三部分禁止
- 禁止滚动条,通过
#div.scrollTop = 0
,即可让滚动条锁定在顶端 - 禁止鼠标滚轮,通过
#div.onwheel=event=>{event.preventDefault()}
,即可阻止滚轮 - 禁止触控,通过
#div.ontouchstart=event=>{event.preventDefault()}
,即可阻止触控
既然能在看得见滚动条的情况下禁止滚动,那能不能在看不见滚动条的情况下允许滚动呢?
于是就又延伸出一个需求。。。
当然也很简单,通过一个尚未加入标准的 CSS 伪元素即可实现
1 | #div::-webkit-scrollBar { |
即可实现在看不见滚动条的情况下允许滚动
自定义事件
那万一我玩得不开心,想自定义一个事件,行不行?
可以!
通过以下方法即可创建一个自定义事件
1 | const event = new CustomEvent(eventName, { detail: config }); |
其中第一个参数表示你的事件名称,第二个参数是一个包含 detail 属性的对象
关于事件的初始化配置,都写在 detail 中
例如
1 | const config={ |
即可创建一个名为 test 的事件,并指示该事件不进行冒泡传递,且不可以被阻止默认动作
创建完成事件后,还要分发事件,才可以让元素上的监听器正常工作,不然你让浏览器怎么触发你设置的事件?2333
要分发事件,首先要选中元素,然后触发他身上的事件
1 | let div = document.querySelector('#div') |
通过以上代码,就可以为 id 为 div 的元素绑定一个 test 事件的监听器,当事件触发时打印 233
然后向这个元素分发事件,触发对应的监听器
事件委托
但是有时候,要为很多相似元素各自绑定相同的监听器
比如一张 10x10 的表格,难道为每个 td 都绑定一个监听器吗?且不说要写 100 行代码,光是 100 个监听器,就卡爆了!
那么这时候,就需要我们的事件委托
事件委托就是把大量相似子项的相似逻辑的监听器,全部取消,然后利用事件冒泡传递的特性,把监听器绑定在父项上,使得监听器既能获得事件发生的确切位置,又大幅简化了代码,优化了性能
比如开头 10x10 表格的例子,就可以将监听器绑定在 table 标签上,借助冒泡传递,获取事件的 target,来处理对应子项的逻辑
显然,这样是很节省内存的,而且即便后续为表格添加项目,依然可以通过 target 得到事件发生的位置,是非常灵活的
所以,事件委托有以下三大优点
- 节省内存,多个监听器变为 1 个监听器
- 动态监听,可以监听未来添加的项目
- 封装,大幅简化代码逻辑,易于调试
感谢阅读