详细剖析 event 几个基本点

Miss朵

事件流

当在某个DOM元素上触发事件以后,DOM是如何派发事件并寻找监听器的呢?这个派发过程就是事件流。

事件从Document(有的浏览器是window,总之是最外层根元素)开始,按照物理层级结构,往下逐级捕获到事件,直到到达目标元素target,再原路冒泡返回。

这个路程中,会按顺序执行所有绑定的对应的监听函数。

先摆个从网上找的图:给一个总体映像。
-1849443303.jpeg
图0-1

抽丝剥茧

前提:
HTML:

<body>
    <div id="c0">
        c0
        <div id="c1">c1</div>
        <div id='c2'>c2
            <div id="c21">c21</div>  
            <div id="c22">c22</div>
            <div id="c23">c23</div>     
        </div>
    </div> 
</body>
<style>
    #c0{margin:10px;padding:10px;background-color: palegoldenrod; position: relative;}
    #c1{margin:20px;background:rgb(165, 252, 194);width:100px;height:30px;}  
    #c2{margin:20px;background:plum;}  
    #c21,#c22,#c23,#c24,#c25{margin:10px;background:peru;width:100px;height:30px;}  
    #c22,#c24{background:gold;} 
    /* #c2{position:absolute;top:100px;} 
    #c23{margin-bottom:0} */
    /* #c2{
        position: relative;
    }
    #c21{
        z-index:2;
    }
    #c21,#c22,#c23{
        position:absolute;
    } */
    /* #c22{
        position: absolute;
        top:20px;
        left:40px;
    }
    #c21{
        z-index:2
    } */
</style>
<script>
    var divs = Array.from(document.getElementsByTagName('div'))
    divs.forEach(element => {
        element.addEventListener('click', function (event) {
            console.log('click:target='+event.target.getAttribute('id')+",currentTarget="+event.currentTarget.getAttribute('id'))
        }, false)
    });
</script>

event对象
监听器在执行时,会传一个event对象给回调函数。event对象里有很多属性,用以传递事件的相关重要信息。其中,有一个target,一个currentTarget,部分事件还有relatedTarget

1、target是谁?
event.target:在哪个DOM元素上触发了事件。它一定是物理空间上能感知该事件,最深层的,离用户最近的元素

以下实验均在chrome上操作。

只在c0上绑定监听click事件,然后依次点击不同区域。(默认useCapture = false,这个后面会分析)
clipboard.png
图1-1
target就是那个直接被点击的DOM元素。

1.1 最深层次 不用讲了。
1.2 物理空间
#c2{position:absolute;top:100px;}
clipboard.png
这些区域并不会触发点击事件。
似乎也能理解为可视区域的意思?

1.3.离用户最近
1) 对元素position做个修改:
#c2{position: relative;}
#c22{position: absolute;top:20px; left:40px;}
clipboard.png
互换下C21,C22 在html中的前后位置,结果与上图完全一致。

2) 将C2的子元素全部position:absolute,完全重叠
#c21,#c22,#c23{position:absolute;}
clipboard.png
离用户最近的取决于子元素在html中的顺序。最后的,在最上面。

3) c23,c22互换顺序,最后的c22就在最上面成为target
clipboard.png

4) 给c21添加index
#c21{z-index:2}
clipboard.png

所以:优先级:相同position下,z-index > 元素顺序。
position > z-index > 元素顺序

2、currentTarget 是谁?
event.currentTarget: target所在物理层级DOM链上,当前eventListener所在的元素。
通俗讲:就是事件流当前所在的对象,也就是注册了对应事件listener的元素。

当将所有元素都绑定上click的listener。 4次点击,右边分别输出4块
clipboard.png
图2-1
与图1-1相比,相同次数点击。两点区别:
(1)、执行次数变多了。之前只有c0监听了click,现在每个元素都监听了click。事件流会执行沿路所有监听函数。
如何区分执行了谁的监听函数?currentTarget就是当前listener所在DOM元素
(2)、所以图2-1的currentTarget不再和图1-1一样始终是c0

再仔细看c22点击,触发监听函数的顺序
由子及父:c22->c2->c0
是不是对事件有点初步理解?

3、冒泡
useCapture = false

事件流严格讲,先后有3个阶段:捕获阶段,进入目标阶段,冒泡阶段。
冒泡就是指冒泡阶段。图0-1的 4->5->6->7步

target.addEventListener(type, listener, useCapture);
要理解冒泡,必须理解监听器注册函数addEventListener的第三个参数:useCapture。

useCapture:是否在捕获阶段执行监听函数。默认值为false,即默认是在冒泡阶段执行监听函数,所以,在图2-2中的顺序是由子及父。

4、捕获
useCapture = true
clipboard.png
useCapture设置为true以后。根据currentTarget,可见函数执行顺序,是由父及子。这就是捕获阶段。

每一次事件在不被拦截的情况下,都会按照物理层理结构,走完捕获、冒泡整个过程。(部分事件除外)

好,一气呵成,理解了事件流了吧。
更多event的属性,见:https://developer.mozilla.org/zh-CN/docs/Web/API/Event

我们再深入一些。

5、阻止冒泡
可以理解为:截断事件流。

e.stopPropagation(),IE则是使用e.cancelBubble = true

5.1 useCapture = false时
clipboard.png
useCapture = false,各个listener元素都阻止了冒泡, target === currentTarget
冒泡阶段,阻止冒泡,停止后续事件流。

5.2 useCapture = true时
clipboard.png
c0:useCapture = true,永远在c0捕获时就执行监听函数。
捕获阶段,阻止子元素继续捕获事件,停止后续事件流,包括冒泡阶段。

6、event.stopImmediatePropagation()
如果有多个相同类型事件的事件监听函数绑定到同一个元素,当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了 event.stopImmediatePropagation() 方法,则当前元素剩下的监听函数将不会被执行。

7、分析几个易混淆的mouseEvent

MouseEvent 派生自 UIEventUIEvent 派生自 Event
所以它集合了3个类的属性。
clipboard.png
详见:https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent

7.1 mousemove
mousemove,在元素内滑动触发。

先只打印target。
clipboard.png
target就是当前鼠标所在最深层次离用户最近的DOM元素。

同一个target触发事件背后,其实执行了多个监听器。如果打印currentTarget的话,console面板会很长了。
clipboard.png
冒泡模式,currentTarget由子及父,循环往复

c21里的滑动效果,就是下图:
clipboard.png

所以如果在一条DOM链上,绑定太多mousemove监听器,开销是很大的。一般都会对mousemove做节流:以一定的时间间隔来执行move事件。还有个概念叫防抖:始终延迟执行直到一段时间内不再触发时才执行一次。根据我的清晰描述,你是否能自己写出节流和防抖函数呢?

7.2 mouseover 和 mouseenter

7.2.1 mouseover
进入目标元素可视区域就触发1次。可以理解为:滑过可视区域。
哪怕进入子元素后再从子元素进入元素,也会再触发1次mouseover事件
与之相对应的事件为 mouseout
clipboard.png

如果是
c0:{position:relative}
c2:{position:absolute;top:100px;}
clipboard.png
图7-2-1-2

7.2.2 mouseenter
进入元素物理空间时触发1次。
只要在元素物理空间内,这个元素都不会再次触发,哪怕进去子元素再出来。
与之相对应的事件为 mouseleave
clipboard.png

不是说事件不阻止就会走完捕获、冒泡全程吗?
为什么看起来好像没有冒泡,并没有出现类似target=c1,currentTarget=c0的记录

对的。mouseenter与mousemove最大的区别就是:mouseenter不会冒泡。而是会向层次结构的每一个元素发送一个mouseenter事件。所以开销很大。

=mouseenter.png
(mouseenter事件)触发时,会向层次结构的每个元素发送一个mouseenter事件。当指针到达文本时,此处将4个事件发送到层次结构的四个元素。

=mouseover.png
一个单独的mouseover事件被发送到DOM树的最深层元素,然后它将层次结构向上冒泡,直到它被处理程序取消或到达根目录。

所以一旦进入enter一个DOM元素的物理空间,在离开leaver之前,不会再次触发该DOM的enter事件。而mouserenter又不会冒泡,这就解释了图7-2-1-2为什么每个target只有一条记录,每个currentTarget也只有一条记录

那鼠标如果反过来走呢?毕竟c23和c2的下边缘重叠的。
clipboard.png
中间那一步,c0 -> c23。会直接产生2条记录,表示进入了c23,c2,而且通过currentTarget发现,并非冒泡触发。往各个层级结构发送事件,才能体现enter这个词的意思。仔细体会下。
细心的同学也发现:mouseenter的 target === currentTarget

另外,请注意,事件派发顺序。当默认useCapture = false时,mouseenter父级监听器会优先执行
如果useCapture = true:
image.png
当useCapture = true时,mouseenter保持了捕获阶段的事件流

另一个例子(useCapture = false)
c0:{position:relative}
c2:{position:absolute;top:100px;}
clipboard.png

7.3 mouseout 与 mouseleave

7.3.1 mouseout:划出可视区域时触发1次。
clipboard.png

7.2.2 mouseleave:离开元素。
mouseleave同mouseenter一样,不会冒泡,离开几个元素,发几个事件。
clipboard.png

clipboard.png
当离开它们时,一个mouseleave事件被发送到层次结构的每个元素。当指针从文本移动到这里表示的最外面的div之外的区域时,这里4个事件会发送到层次结构的四个元素。

clipboard.png
一个单一的鼠标事件mouseout被发送到DOM树最深的元素,然后它冒泡层次,直到它被处理程序取消或到达根。

8、VUE中的事件修饰符

  • .stop : 等价于e.stopPropagation() 阻止冒泡
  • .prevent:等价于e.preventDefault() 阻止默认动作
  • .capture:等价于捕获模式
  • .self : 等价于target === curentTarget:只当在 event.target 是当前元素自身时触发处理函数。
  • .once :只触发一次。
  • .passive:滚动事件的默认行为 (即滚动行为) 将会立即触发。尤其能够提升移动端的性能
阅读 668
18 声望
0 粉丝
0 条评论
18 声望
0 粉丝
宣传栏