事件机制详解
原创文章,未经允许,请勿转载
事件流
当用户鼠标点击或者程序主动触发都会产生事件,比如用户点击了网页,那这个过程到底发生了什么?
此时网页
已经知道了用户点击了鼠标,那么首先它需要知道当前鼠标指针所在的屏幕坐标screenX,screenY
,这个可以通过操作系统 API 得到
- 捕获/碰撞检测
得到屏幕坐标之后,接下来需要知道的就是用户到底点了网页上的具体哪个元素,我们知道网页内所有元素是一颗 dom 树,可以这样理解,渲染到屏幕上就是一个一个层次交叠的方块,每个元素都有自己的坐标xy和宽高
这个信息,其中坐标 xy 可以映射为屏幕上的坐标 xy,我们先把这个信息称之为bound
,代表了该元素在屏幕上的边界信息
此时浏览器会从树的根部遍历整棵树,把所有bound
包含了screenX,screenY
的元素都找出来,假设上图的红点就是我们的点击位置,可以想象为一颗子弹从屏幕外射向屏幕里面,那么我们得到三个元素都和这个子弹发生了碰撞:body > div.wrap > div.content
,这个过程称之为捕获
阶段
- 最里层的元素
div.content
就是我们event
参数里的event.target
event
参数里的path
存储了body 、 div.wrap 、 div.content
这三个元素
在遍历的过程中我们会与上面三个元素都有接触,接触过程中,我们会找下当前这个元素身上有没有绑定用于接收捕获
阶段的事件侦听器,如果有则调用之。
addEventListener 方法原型是
addEventListener(type, listener[, useCapture]);
第三个参数如果为true
则代表用来接受捕获
阶段的事件
每个元素身上的侦听器存储信息可以简化如下:
{
'click':[listener1,listener2],
'mousemove':[listener3,listener4],
...
}
这个元素click事件有两个事件侦听器,依次执行listener1
、listener2
event参数里的
event.currentTarget
则是当前元素
假设在执行
listener1
的过程中,侦听器内代码如果调用了event.stopImmediatePropagation();
那么会停止执行后面的listener2
- 冒泡阶段
冒泡阶段则是从最里层往外传播的过程,在找到目标元素div.content
之后,我们会往上冒泡,依次与div.content > div.wrap > body
接触,同理,在接触的过程中会调用元素身上的事件侦听器,调用过程和上面捕获阶段描述的一样
addEventListener 方法原型是
addEventListener(type, listener[, useCapture]);
第三个参数如果为false
或者不传,则代表用来接受冒泡
阶段的事件
侦听器内部如果调用了event.stopPropagation();
则会阻止继续冒泡,本例中假如在div.content
的listener中有调用stopPropagation
则在div.wrap
和body
身上的侦听器都不会被执行了
在侦听器内部调用了event.preventDefault();
会阻止元素的默认事件响应,比如在checkbox上我们增加一个click事件侦听,然后侦听器内部调用event.preventDefault();
,那么这个checkbox不会发生状态翻转
事件侦听器
- 调用了元素的
addEventListener
注册了事件侦听器之后,这个元素就存储了侦听器本身的引用,所以在移除元素之前,需要调用removeListener
断开引用,以免造成内存回收器无法回收元素所占用的内存
在 Vue 程序里面,需要在
beforeDestroy
钩子里面移除掉之前手动注册的事件侦听器
- 事件传播过程中,调用元素身上的事件侦听器,会把
this
绑定为当前元素,所以在lisntener内部一定要特别处理this,或者使用箭头函数,比如:
//必须找地方存储listener引用,用于后面的remove,不能这样:el.addEventListener('click', (event) => { ... })
const onClickListener = (event) => {
console.log(event.currentTarget)
}
//附加
el.addEventListener('click', onClickListener)
//移除
el.removeEventListener('click', onClickListener)
Vue 里面使用
@event
注册的事件,Vue已经帮我们处理好了this引用,this会绑定为当前Vue组件
代码触发事件
代码主动在某元素上使用dispatchEvent
派发事件,不会有捕获阶段,只会有冒泡阶段(可以选择关闭冒泡)
比如我们在
div.content
上派发一个自定义事件,其所有父元素都会收到这个自定义事件
可以选择关闭冒泡,如下所示
el.dispatchEvent(new Event('myEvent', { bubbles: false }))
event bus 事件总线
一个系统里面经常使用事件总线来集合各个模块的自定义事件,事件总线对象应该拥有注册侦听器、触发事件、派发事件
等功能,然后各个模块引用同一个事件总线的实例
比如模块
A
更改了全局状态S
,则在eventBus上触发一个更改事件,模块B
先在eventBus上附加一个事件侦听器,就会收到事件通知,接收到状态S
的更改结果,这种事件驱动的设计,杜绝了模块A
和B
直接的直接交互,解耦了模块间的联系,即使模块B
不存在,模块A
触发事件也不会造成异常
来源:悠游悠游,2019-06-12,原文地址:https://yymmss.com/p/explain-event.html