DOM 事件机制

弱弱地冒一个泡

经常听到冒泡捕获,那到底是什么东西呢?

事件模型

首先就要介绍一下 W3C DOM 事件模型

事件总体上分为三个阶段

  1. 捕获阶段,事件从父到子向下传递
  2. 目标阶段,事件传递到达发生事件的目标点
  3. 冒泡阶段,事件从子到父向上传递

不论是什么事件,没有特殊设置的情况下,都会按顺序经历这三个阶段

除非该事件被取消冒泡,才会在取消冒泡之后停止传递

前两个阶段不能停止传递

通过 on 赋值可以为元素绑定监听器,此时监听器一定是在事件冒泡阶段发生,且后赋值的监听器会覆盖之前的监听器

例如 button.onclick=()=>{}

通过 addEventListener(eventName, listener, useCapture, priority, useWeakReference) 可以设置监听器是在事件的冒泡阶段还是捕获阶段发生,该方式可以为同一个元素的同一个事件设置多个监听

useCapture 默认值是 false,表示监听器在事件冒泡阶段发生,反之当为 true 时即表示在捕获阶段发生

priority 默认值是 0,表示所有同级的监听器按注册顺序进行。可以为个别监听器设置更高的优先级,优先级高的会在该阶段先执行,同优先级的依然按注册顺序进行

useWeakReference 默认值是 false,表示设置监听器为强引用,使得监听器不被垃圾回收,反之 true 则是允许回收

当要取消监听器、或要重新设定优先级时,可以使用 removeEventListener() 来取消监听器,之后再重新设定

每当事件触发一个监听器的时候,都会向监听器内传入两个默认参数

一个是 this,表示事件现在所处的元素

一个是 event,包含事件的完整信息

target 与 currentTarget

通过在监听器中打印 event,可以发现两件事

  1. target != currentTarget
  2. 如果先保存 event,事件结束后再打印,则会发现 target 变成了 null

对于第一点,target 是事件发生的最小元素,也就是唯一有目标阶段的元素

currentTarget 则是事件现在所处的位置,是传入监听器的 this

对于第二点,则涉及到事件消亡的知识,篇幅过大,此处不介绍了

target 元素 事件先后

刚才说到 target 元素是唯一有目标阶段的元素,那么什么是目标阶段?

实际上,对于 target 元素来说,目标阶段就是捕获冒泡连续发生,不像事件传递路径上别的元素是分开发生的

  • 哦哦,那我懂了,所以对 target 元素来说,监听器也是按照设置好的捕获冒泡顺序执行的吧
  • 不!

此时监听器不再区分捕获和冒泡,统一按照设置顺序发生

取消冒泡

那有时候我想到此为止,不想打扰父级元素,怎么办呢?

可以在需要中断的元素的冒泡阶段的监听器中,执行

1
e.propagetion()

此时就可以阻止事件继续冒泡

默认动作

既然冒泡可以阻止,那默认的事件能不能阻止呢?比如我现在有个 a 标签,想做单页面应用的 tab 页跳转,要是按照默认的,可就跳到新页面去了

答案是可以!

在事件中执行

1
e.preventDefault()

就可以阻止默认动作的发生了!

禁用滚动

奇怪,说好的阻止默认动作,怎么我在滚动条上不能 prevent,你骗我!

等下等下,你真的找准是什么东西产生了滚动条了吗?

有时候你看着是你的元素产生了滚动条,但说不定是 body 产生的!

找准了之后,我们来禁用滚动条吧——毕竟滚动条其实只是 CSS 产生的东西

禁用滚动一般分两种:

  1. 看不见滚动条
  2. 看得见滚动条

对于第一种,其实只需要设置 overflow: hidden 就可以实现了

对于第二种,比较复杂一些

首先能看见滚动条,说明 overflow 至少是 auto,甚至是 scroll

这时候通过划拉滚动条,或者鼠标滚轮,甚至是触屏上的手指,都可以让页面滚动

这种时候就要分三部分禁止

  1. 禁止滚动条,通过 #div.scrollTop = 0,即可让滚动条锁定在顶端
  2. 禁止鼠标滚轮,通过 #div.onwheel=event=>{event.preventDefault()},即可阻止滚轮
  3. 禁止触控,通过 #div.ontouchstart=event=>{event.preventDefault()},即可阻止触控

既然能在看得见滚动条的情况下禁止滚动,那能不能在看不见滚动条的情况下允许滚动呢?

于是就又延伸出一个需求。。。

当然也很简单,通过一个尚未加入标准的 CSS 伪元素即可实现

1
2
3
#div::-webkit-scrollBar {
display:none;
}

即可实现在看不见滚动条的情况下允许滚动

自定义事件

那万一我玩得不开心,想自定义一个事件,行不行?

可以!

通过以下方法即可创建一个自定义事件

1
const event = new CustomEvent(eventName, { detail: config });

其中第一个参数表示你的事件名称,第二个参数是一个包含 detail 属性的对象

关于事件的初始化配置,都写在 detail 中

例如

1
2
3
4
5
const config={
bubbles: false,
cancelable: false
}
const event = new CustomEvent('test', { detail: config });

即可创建一个名为 test 的事件,并指示该事件不进行冒泡传递,且不可以被阻止默认动作

创建完成事件后,还要分发事件,才可以让元素上的监听器正常工作,不然你让浏览器怎么触发你设置的事件?2333

要分发事件,首先要选中元素,然后触发他身上的事件

1
2
3
let div = document.querySelector('#div')
div.addEventListener('test',console.log(233))
div.dispatchEvent('test')

通过以上代码,就可以为 id 为 div 的元素绑定一个 test 事件的监听器,当事件触发时打印 233

然后向这个元素分发事件,触发对应的监听器

事件委托

但是有时候,要为很多相似元素各自绑定相同的监听器

比如一张 10x10 的表格,难道为每个 td 都绑定一个监听器吗?且不说要写 100 行代码,光是 100 个监听器,就卡爆了!

那么这时候,就需要我们的事件委托

事件委托就是把大量相似子项的相似逻辑的监听器,全部取消,然后利用事件冒泡传递的特性,把监听器绑定在父项上,使得监听器既能获得事件发生的确切位置,又大幅简化了代码,优化了性能

比如开头 10x10 表格的例子,就可以将监听器绑定在 table 标签上,借助冒泡传递,获取事件的 target,来处理对应子项的逻辑

显然,这样是很节省内存的,而且即便后续为表格添加项目,依然可以通过 target 得到事件发生的位置,是非常灵活的

所以,事件委托有以下三大优点

  1. 节省内存,多个监听器变为 1 个监听器
  2. 动态监听,可以监听未来添加的项目
  3. 封装,大幅简化代码逻辑,易于调试

感谢阅读

--It's the end.Thanks for your read.--