javascript中mouseenter与mouseover的异同
不知道⼤家在⾯试或者⼯作过程中有没有被mouseover和mouseenter (对应的是mouseout和mouseleave )事件所困扰。⾃⼰之前在⾯试的时候就有被问到诸如mouseover和mouseenter事件的异同之类的问题?当时没有答出来,⼀直也对这两个事件有点模糊不清,趁着最近正在读,准备写⼀篇这⽅⾯的⽂章,如果有错误,请⼤家指正。
mouseenter与mouseover的异同?
要说清楚mouseenter与mouseover有什么不同,也许可以从两⽅⾯去讲。
是否⽀持冒泡事件的触发时机
先来看⼀张图,对这两个事件有⼀个简单直观的感受。
再看看官⽹对mouseenter的解释
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事件来说,relatedTarge
t只可能是
ul的⽗元素wrap(移⼊ul时,此时也是触发mouseenter事件的时候, 其实不⼀定,后⾯会说明 ),或者ul元素本⾝(在其⼦元素上移出时),⼜或者是⼦元素本⾝(直接从⼦元素A移动到⼦元素B)。
relatedTarget
根据上⾯的描述,我们可以对relatedTarget的值进⾏判断:如果值不是⽬标元素,也不是⽬标元素的⼦元素,就说明⿏标已移⼊⽬标元素⽽不是在元素内部移动。
条件1:不是⽬标元素很好判断e.relatedTarget !== target(⽬标元素)
条件2:不是⽬标元素的⼦元素,这个应该怎么判断呢?
这⾥需要介绍⼀个新的api , 表⽰传⼊的节点是否为该节点的后代节点, 如果 otherNode 是 node 的后代节点或是 node 节点本⾝.则返回true , 否则返回 false
⽤法案例
<ul class="list">
<li class="item">1</li>
<li>2</li>
</ul>
<div class="test"></div>
let $list = document.querySelector('.list')
let $item = document.querySelector('.item')
let $test = document.querySelector('.test')
$ains($item) // true
$ains($test) // false
$ains($list) // true
那么利⽤contains这个api我们便可以很⽅便的验证条件2,接下来我们封装⼀个contains(parent, node)函数,专门⽤来判断node是不是parent的⼦节点
let contains = function (parent, node) {
return parent !== node && ains(node)
}
⽤我们封装过后的contains函数再去试试上⾯的例⼦
contains($list, $item) // true
contains($list, $test) // false
contains($list, $list) // false (主要区别在这⾥)
这个⽅法很⽅便地帮助我们解决了模拟mouseenter事件中的条件2,但是悲催的ains(otherNode) ,具有浏览器兼容性,在⼀些低级浏览器中是不⽀持的,为了做到兼容我们再来改写⼀下contains⽅法
let contains = ains ? function (parent, node) {
return parent !== node && ains(node)
} : function (parent, node) {
let result = parent !== node
if (!result) { // 排除parent与node传⼊相同的节点
return result
}
if (result) {
while (node && (node = node.parentNode)) {
if (parent === node) {
return true
}
}
}
return false
}
说了这么多,我们来看看⽤mouseover事件模拟mouseenter的最终代码
// callback表⽰如果执⾏mouseenter事件时传⼊的回调函数
let emulateEnterOrLeave = function (callback) {
return function (e) {
let relatedTarget = e.relatedTarget
nodeselectorif (relatedTarget !== this && !contains(this, relatedTarget)) {
callback.apply(this, arguments)
}
}
}
好了,我们已经通过mouseove事件完整的模拟了mouseenter事件,但是反过头来看看
对于ul上添加的mouseover事件来说,relatedTarget只可能是
ul的⽗元素wrap(移⼊ul时,此时也是触发mouseenter事件的时候, 其实不⼀定,后⾯会说明 ),或者ul元素本⾝(在其⼦元素上移出时),⼜或者是⼦元素本⾝(直接从⼦元素A移动到⼦元素B)。
我们通过排查2和3,最后只留下1,也就是mouseenter与mouseover事件⼀起触发的时机。既然这样我们为什么不像这样判断呢?
target.addEventListener('mouseover', function (e) {
if (e.relatedTarget === this.parentNode) {
// 执⾏mouseenter的回调要做的事情
}
}, false)
这样不是更加简单吗?,何必要折腾通过排查2和3来做?
原因是,target的⽗元素有⼀定的占位空间的时后,我们这样写是没有太⼤问题的,但是反之,这个时候e.relatedTarget就可能是target元素的⽗元素,⼜祖先元素中的某⼀个。我们⽆法准确判断
⽤mouseout模拟mouseleave事件
当mouseout被激活时,relatedTarget表⽰⿏标离开⽬标元素时,进⼊了哪个元素,我们同样可以对relatedTarget的值进⾏判断:如果值不是⽬标元素,也不是⽬标元素的⼦元素,就说明⿏标已移出⽬标元素
我们同样可以⽤上⾯封装的函数完成
// callback表⽰如果执⾏mouseenter事件时传⼊的回调函数
let emulateEnterOrLeave = function (callback) {
return function (e) {
let relatedTarget = e.relatedTarget
if (relatedTarget !== this && !contains(this, relatedTarget)) {
callback.apply(this, arguments)
}
}
}
结尾
⽂中也许有些观点不够严谨,欢迎⼤家拍砖。