不知道大家在面试或者工作过程中有没有被 mouseover 和 mouseenter (对应的是 mouseout 和 mouseleave )事件所困扰。自己之前在面试的时候就有被问到诸如mouseover和mouseenter事件的异同之类的问题?当时没有答出来,一直也对这两个事件有点模糊不清,趁着最近正在读 zepto源码 ,准备写一篇这方面的文章,如果有错误,请大家指正。
mouseenter与mouseover的异同?要说清楚mouseenter与mouseover有什么不同,也许可以从两方面去讲。
是否支持冒泡 事件的触发时机
先来看一张图,对这两个事件有一个简单直观的感受。
再看看官网对mouseenter的解释mouseenter | onmouseenter event
The event fires only if the mouse pointer is outside the boundaries of the object and the user moves the mouse pointer inside the boundaries of the object. If the mouse pointer is currently inside the boundaries of the object, for the event to fire, the user must move the mouse pointer outside the boundaries of the object and then back inside the boundaries of the object.
大概意思是说:当鼠标从元素的边界之外移入元素的边界之内时,事件被触发。而当鼠标本身在元素边界内时,要触发该事件,必须先将鼠标移出元素边界外,再次移入才能触发。(英语比较渣:no_mouth:,凑合看哈)
Unlike the onmouseover event, the onmouseenter event does not bubble.
大概意思是:和mouseover不同的是,mouseenter不支持事件冒泡 (英语比较渣:no_mouth:,凑合看哈)
由于mouseenter不支持事件冒泡,导致在一个元素的子元素上进入或离开的时候会触发其mouseover和mouseout事件,但是却不会触发mouseenter和mouseleave事件
我们用一张动图来看看他们的区别(或者点击该链接体验)。
我们给左右两边的ul分别添加了 mouseover 和 mouseenter 事件,当鼠标进入左右两边的ul时, mouseover 和 mouseenter 事件都触发了,但是当移入各自的子元素li的时候,触发了左边ul上的mouseover事件,然而右边ul的mouseenter事件没有被触发。
造成以上现象本质上是 mouseenter 事件不支持冒泡所致。
如何模拟mouseenter事件。
可见mouseover事件因其具有冒泡的性质,在子元素内移动的时候,频繁被触发,如果我们不希望如此,可以使用mouseenter事件代替之,但是早期只有ie浏览器支持该事件,虽然现在大多数高级浏览器都支持了mouseenter事件,但是难免会有些兼容问题,所以如果可以自己手动模拟,那就太好了。
关键因素: relatedTarget 要想手动模拟mouseenter事件,需要对mouseover事件触发时的事件对象event属性relatedTarget了解。
relatedTarget事件属性返回与事件的目标节点相关的节点。 对于mouseover事件来说,该属性是鼠标指针移到目标节点上时所离开的那个节点。 对于mouseout事件来说,该属性是离开目标时,鼠标指针进入的节点。 对于其他类型的事件来说,这个属性没有用。
重新回顾一下文章最初的那张图,根据上面的解释,对于ul上添加的mouseover事件来说,relatedTarget只可能是
ul的父元素wrap(移入ul时,此时也是触发mouseenter事件的时候, 其实不一定,后面会说明 ), 或者ul元素本身(在其子元素上移出时), 又或者是子元素本身(直接从子元素A移动到子元素B)。
relatedTarget根据上面的描述,我们可以对relatedTarget的值进行判断:如果值不是目标元素,也不是目标元素的子元素,就说明鼠标已移入目标元素而不是在元素内部移动。
条件1: 不是目标元素 很好判断 e.relatedTarget !== target(目标元素)
条件2:不是目标元素的子元素,这个应该怎么判断呢?
ele.contains
这里需要介绍一个新的api , 表示传入的节点是否为该节点的后代节点, 如果 otherNode 是 node 的后代节点或是 node 节点本身.则返回true , 否则返回 false
用法案例
<ul> <li>1</li> <li>2</li> </ul> <div></div>
let $list = document.querySelector('.list') let $item = document.querySelector('.item') let $test = document.querySelector('.test') $list.contains($item) // true $list.contains($test) // false $list.contains($list) // true
那么利用contains这个api我们便可以很方便的验证条件2,接下来我们封装一个 contains(parent, node) 函数,专门用来判断 node 是不是 parent 的子节点
let contains = function (parent, node) { return parent !== node && parent.contains(node) }
用我们封装过后的 contains 函数再去试试上面的例子
contains($list, $item) // true contains($list, $test) // false contains($list, $list) // false (主要区别在这里)