提示:本文写于 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 都会被触发:
- 鼠标离开 parent
- 鼠标从 child 移动到 parent
- 鼠标从 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
欢迎分享你的看法,也欢迎补充不同的实践经验。