本文详细解析了JavaScript中mouseover和mouseout事件在元素嵌套时因冒泡机制导致的多次触发问题。通过对比mouseenter和mouseleave的行为差异,深入探讨了利用relatedTarget、contains及compareDocumentPosition实现兼容各浏览器的自定义事件封装方法,提供了完整的源码实现与测试验证。
问题描述
问题描述
期望实现的效果是:当鼠标移入id1区域时,id2显示;当鼠标离开id1区域时,id2隐藏。然而实际运行中出现了异常现象。
1. 当鼠标从id1区域移动到id2区域时,id2会先由显示状态变为隐藏,随后又立即变为显示。
2. 当鼠标从id2区域移回id1区域时,id2同样会经历由显示变为隐藏,再变为显示的过程。
期望的行为是:只要鼠标在id1或id2区域内移动,id2应保持持续显示状态,不出现闪烁或隐藏。
<script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.js"></script>
<div id="id1" style="width:800px; height:400px; background-color:#F23">
<div id="id2" style="width:400px; height:300px; background-color:#0F8; display:none;">
</div>
</div>
<script type="text/javascript">
$("#id1").mouseover(function(){
$(this).children().fadeIn(1000);
}).mouseout(function(){
$(this).children().fadeOut(1000);
});
</script>

问题解决办法
问题解决办法
初步分析表明,当鼠标从id1移向id2时,鼠标离开id1触发了id1的mouseout事件,导致id2隐藏;随后鼠标进入id2,触发id2的mouseover事件,该事件冒泡至id1,再次触发id1的mouseover事件,使id2重新显示。反之,当鼠标从id2移回id1时,离开id2触发mouseout,冒泡至id1导致id2隐藏;进入id1触发mouseover,使id2再次显示。
由此可见,核心问题在于需要阻止id1在鼠标移入子元素id2时的mouseout事件,以及阻止id2在鼠标移出时冒泡至id1。仅依靠阻止事件冒泡无法彻底解决此问题。
为了解决这一交互问题,jQuery提供了mouseenter和mouseleave事件。将原有的JS代码修改为使用这两个事件后,问题得到了很好的解决。
$("#id1").mouseenter(function(){
$(this).children().fadeIn(1000);
}).mouseleave(function(){
$(this).children().fadeOut(1000);
});
许多资料介绍了mouseenter、mouseleave与mouseover、mouseout的区别,以下引用了部分说明。
/*********************************************************/
1. mouseover与mouseenter
无论鼠标指针穿过被选元素本身还是其子元素,都会触发mouseover事件。
只有当鼠标指针直接穿过被选元素本身时,才会触发mouseenter事件。
2. mouseout与mouseleave
无论鼠标指针离开被选元素本身还是其任何子元素,都会触发mouseout事件。 只有当鼠标指针直接离开被选元素本身时,才会触发mouseleave事件。
/*********************************************************/
现象确实如此,但上述描述的过程略显模糊,以下是我的理解:
当鼠标指针移入被选元素时,会触发mouseover事件。当鼠标指针从被选元素移向其子元素时,首先触发被选元素的mouseout事件,随后子元素的mouseover事件冒泡至被选元素。这相当于被选元素先执行了一次mouseout,接着又执行了一次mouseover。
为了验证这一机制,将代码修改如下:
<script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.js"></script>
<div id="id1" style="width:800px; height:400px; background-color:#F23">
<div id="id2" style="width:400px; height:300px; background-color:#0F8; position:absolute; top:300px;">
</div>
</div>
<script type="text/javascript">
$("#id1").mouseover(function(){
//$(this).children().fadeIn(1000);
console.log('a');
}).mouseout(function(){
//$(this).children().fadeOut(1000);
console.log('b');
});
</script>
鼠标从页面移动到id1,然后由id1移动到id2上,控制台输出结果如下图所示。
从输出可以看出,id1先后调用了mouseover、mouseout、mouseover事件,这与上述分析完全一致。
mouseenter与mouseleave实现分析
mouseenter与mouseleave实现分析
原理分析
从上述分析可知,要实现mouseenter与mouseleave的效果,关键在于:当鼠标从被选元素移动到其子元素时,被选元素不执行mouseout事件,也不执行子元素冒泡过来的mouseover事件;当鼠标从被选元素的子元素移回被选元素时,被选元素不执行mouseover事件,也不执行子元素冒泡过来的mouseout事件。
要实现上述效果,我们需要利用event对象的一个属性relatedTarget,该属性用于判断mouseover和mouseout事件目标节点的相关节点。简单来说,当触发mouseover事件时,relatedTarget代表鼠标刚刚离开的那个节点;当触发mouseout事件时,它代表鼠标移向的那个对象。由于MSIE不支持此属性,但其有替代属性fromElement和toElement。此外,我们还需要contains方法来判断一个对象是否包含在另一个对象中。
因此,当鼠标移动时,只需判断以下两条规则即可:
1. 调用mouseover时,只需判断relatedTarget是否是被选元素的子元素。如果是,则不执行操作(相当于从被选元素子元素移动到被选元素时,不执行mouseover;或从被选元素移动到其子元素时,不执行冒泡过来的mouseover);
2. 调用mouseout时,只需判断relatedTarget是否是被选元素的子元素。如果是,则不执行操作(相当于从被选元素子元素移动到被选元素时,不执行子元素冒泡过来的mouseout;或从被选元素移动到其子元素时,不执行冒泡过来的mouseout);
实现过程
实现过程
判断两个元素是否存在包含关系
jquery中封装了contains函数如下:
可以简化为如下形式:
//判断两个a中是否包含b
function contains(a,b){
return a.contains ? a != b && a.contains(b) :!!(a.compareDocumentPosition(b) & 16);
}
compareDocumentPosition介绍
该方法是DOM Level 3 specification的一部分,允许确定两个DOM Node之间的相互位置。该方法比.contains()更强大,可用于将DOM Node排序为详细精确的顺序。NodeA.compareDocumentPosition(NodeB)返回的信息描述如下:
比特 序号 意义

通过上述分析即可理解为何采用 a.compareDocumentPosition(b) & 16 的形式。若节点 A 包含节点 B,该表达式返回 16,此时 16 & 16 结果为 1;而在其他情形下,运算结果均为 0。
获取兼容性的relatedTarget
为了兼容各种浏览器,参考jquery源码,写出如下代码来获取mouseover和mouseout事件目标节点的相关节点属性relatedTarget。
function getRelated(e){
var related;
var type=e.type.toLowerCase();//这里获取事件名字
if(type=='mouseover'){
related=e.relatedTarget||e.fromElement
}else if(type='mouseout'){
related=e.relatedTarget||e.toElement
}
return related;
}
改进mouseover和mouseout
通过改进mouseover和mouseout以实现mouseenter与mouseleave的效果,完整代码如下。
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div id="id1" style="width:800px; height:400px; background-color:#F23">
<div id="id2" style="width:400px; height:300px; background-color:#0F8; position:absolute; top:300px;">
</div>
</div>
<script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script type="text/javascript">
//判断两个a中是否包含b
function contains(a,b){
return a.contains ? a != b && a.contains(b) :!!(a.compareDocumentPosition(b) & 16);
}
function getRelated(e){
var related;
var type=e.type.toLowerCase();//这里获取事件名字
if(type=='mouseover'){
related=e.relatedTarget||e.fromElement
}else if(type='mouseout'){
related=e.relatedTarget||e.toElement
}
return related;
}
$(function(){
$("#id1").mouseover(function(e){
//判断鼠标从哪移到id1上面
var related=getRelated(e);
//如果related是id1的子元素id2,即从子元素id2移动到id1,或是related为id1,即从id1移动到其子元素id2上面,则不进行任何操作,否则进行相应的操作
if(this!=related && !contains(this,related)){
console.log('mouseover');
}
}).mouseout(function(e){
//判断鼠标要从id1上面移动到哪去?
var related=getRelated(e);
//如果related是id1,即当id1从其子元素移动到id1上,或是related是id2,即从id1上移动到其子元素,不进行任何操作,否则进行相应的操作
if(this !=related && !contains(this,related)){
console.log('mouseout');
}
});
});
</script>
</body>
</html>
测试,鼠标移动路线如下图所示。
从控制台输出可以看出,此时的mouseover和mouseout已经完全具备mouseenter与mouseleave的效果。
代码的封装
代码的封装
如果每次进行此类操作都需要加载jQuery或编写大量代码,将十分繁琐。为了便于后续操作,进行了适当封装,模拟jQuery生成自己的mouseenter与mouseleave。代码封装到dqMouse.js文件中,如下:
(function(w){
var dqMouse = function(obj) {
// 函数体
return new dqMouse.fn.init(obj);
}
dqMouse.fn = dqMouse.prototype = {
// 扩展原型对象
obj:null,
dqMouse: "1.0.0",
init: function(obj) {
this.obj=obj;
return this;
},
contains:function(a,b) {
return a.contains ? a != b && a.contains(b) :!!(a.compareDocumentPosition(b) & 16);
},
getRelated:function(e) {
var related;
var type=e.type.toLowerCase();//这里获取事件名字
if(type=='mouseover'){
related=e.relatedTarget||e.fromElement
}else if(type='mouseout'){
related=e.relatedTarget||e.toElement
}
return related;
},
over:function(fn){
var obj=this.obj;
var _self=this;
obj.onmouseover=function(e){
var related=_self.getRelated(e);
if(this!=related && !_self.contains(this,related)){
fn();
}
}
return _self;
},
out:function(fn){
var obj=this.obj;
var _self=this;
obj.onmouseout=function(e){
var related=_self.getRelated(e);
if(obj!=related && !_self.contains(obj,related)){
fn();
}
}
return _self;
}
}
dqMouse.fn.init.prototype = dqMouse.fn;
window.dqMouse = window.$$= dqMouse;
})(window);
调用的源文件如下:
<div id="id1" style="width:800px; height:400px; background-color:#F23">
<div id="id2" style="width:400px; height:300px; background-color:#0F8; position:absolute; top:300px;">
</div>
</div>
<script type="text/javascript" src="dqMouse.js"></script>
<script type="text/javascript">
var id1=document.getElementById('id1');
$$(id1).over(function(){
console.log('mouseover');
}).out(function(){
console.log('mouseout');
});
</script>
以上所述是小编给大家介绍的JS中mouseover和mouseout多次触发问题如何解决的相关内容,希望对大家有所帮助!