Fork me on GitHub

事件机制详解

原创文章,未经允许,请勿转载

事件流

当用户鼠标点击或者程序主动触发都会产生事件,比如用户点击了网页,那这个过程到底发生了什么?

graph LR 用户-->鼠标 鼠标-->操作系统 操作系统-->浏览器 浏览器-->网页

此时网页已经知道了用户点击了鼠标,那么首先它需要知道当前鼠标指针所在的屏幕坐标screenX,screenY,这个可以通过操作系统 API 得到

  • 捕获/碰撞检测

得到屏幕坐标之后,接下来需要知道的就是用户到底点了网页上的具体哪个元素,我们知道网页内所有元素是一颗 dom 树,可以这样理解,渲染到屏幕上就是一个一个层次交叠的方块,每个元素都有自己的坐标xy和宽高这个信息,其中坐标 xy 可以映射为屏幕上的坐标 xy,我们先把这个信息称之为bound,代表了该元素在屏幕上的边界信息

body {x:0,y:0,w:800,h:250} div.wrap {x:50,y:50,w:500,h:150} div.content {x:95,y:140,w:400,h:80}

此时浏览器会从树的根部遍历整棵树,把所有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事件有两个事件侦听器,依次执行listener1listener2

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.wrapbody身上的侦听器都不会被执行了

在侦听器内部调用了event.preventDefault();会阻止元素的默认事件响应,比如在checkbox上我们增加一个click事件侦听,然后侦听器内部调用event.preventDefault(); ,那么这个checkbox不会发生状态翻转

Edit event.preventDefault()

事件侦听器

  • 调用了元素的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的更改结果,这种事件驱动的设计,杜绝了模块AB直接的直接交互,解耦了模块间的联系,即使模块B不存在,模块A触发事件也不会造成异常

来源:悠游悠游,2019-06-12,原文地址:https://yymmss.com/p/explain-event.html