数字营销 · Web开发 · 基础设施

Javascript中onmouseout相关问题的分析

通过分析父子元素之间的 mouseout 触发过程,解决鼠标在菜单内部移动时重复触发事件的问题。

提示:本文写于 2013 年,文中涉及的事件模型、浏览器兼容处理及代码均基于当时的 Web 开发环境,请结合文章发布时间阅读。

问题是这样的。

前一段时间需要做一个二级菜单,为了保证浏览器的兼容性,需要使用 mouseover 和 mouseout 来完成二级菜单的显示和隐藏。

但是,在使用 mouseout 的时候发现了一个比较奇怪的问题:

当鼠标在二级菜单的父元素和子元素之间移动时,父元素的 mouseout 事件会被触发很多次。

下面用一个简单的例子进行模拟:

<!DOCTYPE HTML>
<html>
<head>

<style>
#parent {
width:500px;
height:500px;
position:absolute;
left:50%;
top:50%;
margin-left:-250px;
margin-top:-250px;
background:red;
}

#child {
width:300px;
height:300px;
background:blue;
margin:100px;
}
</style>

<script>

function outhandler(e, obj){

var e = e || window.event; // 事件兼容

// 获取鼠标离开时所到达的对象
var target = e.relatedTarget || e.toElement;

// 如果鼠标仍然在当前对象内部,则退出
if(obj == target || obj.contains(target)){
stopBubble(e);
return;
}

alert("a");
}

function stopBubble(e){

if(e && e.stopPropagation){
// 非 IE 浏览器
e.stopPropagation();
}else{
// IE 浏览器
window.event.cancelBubble = true;
}

}

</script>

</head>

<body>

<div id="parent" onmouseout="outhandler(event,this)">
<div id="child">
</div>
</div>

</body>
</html>

运行之后,可以发现下面三种情况下,parent 的 mouseout 都会被触发:

  1. 鼠标离开 parent
  2. 鼠标从 child 移动到 parent
  3. 鼠标从 parent 移动到 child

而我想要的效果是:

只要鼠标还在 parent 内部,包括它的后代元素,mouseout 就不应该被触发。

显然,只有第一种情况符合要求,那么第二种和第三种为什么也会触发父元素的 mouseout 呢?

鼠标从 child 移动到 parent

先来看第二种情况。

熟悉事件冒泡的可能都知道,DOM 事件会随着事件的产生从子元素一直向上冒泡。

如果顶层的父元素上绑定了相应的事件处理函数,那么事件冒泡到父元素之后,父元素上的处理函数也会被执行。

因此,当鼠标从 child 移动到 parent 的时候,虽然我们感觉鼠标还是在 parent 里面,但是 mouseout 事件已经在子元素上产生,并且向父元素冒泡了。

要避免这种情况,比较简单的方法是在子元素上注册 mouseout 事件处理函数,然后阻止事件继续向上冒泡。

但这种方法有一个问题:

子元素的数量是不可控的。

以后可能随时增加新的子元素,如果每个子元素都需要单独处理,就会比较麻烦,也不利于扩展。

鼠标从 parent 移动到 child

再来看第三种情况。

这个情况其实比较容易理解:

鼠标从父元素移动到子元素,本身也算是从当前元素移出。

所以父元素的 mouseout 自然会被触发。

那么,要避免这种情况,就需要判断鼠标当前移动到的目标元素是否仍然属于 parent。

如果移动到的元素还在 parent 内部,那么就不执行 mouseout 的处理函数。

解决办法

把第二种和第三种情况结合起来,其实可以直接采用第三种办法:

判断鼠标离开时所到达的目标元素是否仍然在当前对象内部。

如果仍然在当前对象内部,就直接退出函数。

完整代码如下:

<!DOCTYPE HTML>
<html>
<head>

<style>
#parent {
width:500px;
height:500px;
position:absolute;
left:50%;
top:50%;
margin-left:-250px;
margin-top:-250px;
background:red;
}

#child {
width:300px;
height:300px;
background:blue;
margin:100px;
}
</style>

<script>

function outhandler(e, obj){

var e = e || window.event; // 事件兼容

// 获取鼠标离开时所到达的对象
var target = e.relatedTarget || e.toElement;

// 如果鼠标仍然在当前对象内部,则退出
if(obj == target || obj.contains(target)){
stopBubble(e);
return;
}

alert("a");
}

function stopBubble(e){

if(e && e.stopPropagation){
// 非 IE 浏览器
e.stopPropagation();
}else{
// IE 浏览器
window.event.cancelBubble = true;
}

}

</script>

</head>

<body>

<div id="parent" onmouseout="outhandler(event,this)">
<div id="child">
</div>
</div>

</body>
</html>

这里主要做了两件事情。

首先通过:

var target=e.relatedTarget ||e.toElement;

获取鼠标离开当前元素之后所到达的对象。

然后判断:

if(obj==target||obj.contains(target)){
return;
}

如果目标对象本身就是当前对象,或者目标对象属于当前对象的子元素,那么说明鼠标实际上仍然在 parent 内部。

这种情况下就不执行后面的处理。

只有当鼠标真正离开整个 parent,目标元素不属于 parent 时,才执行:

alert("a");

这样就可以避免在父元素和子元素之间移动鼠标时,频繁触发父元素的 mouseout 事件。

这个问题在制作二级菜单的时候比较常见,理解 mouseout、事件冒泡以及 relatedTarget 之间的关系之后,处理起来就简单多了。

评论0

欢迎分享你的看法,也欢迎补充不同的实践经验。