应优先使用 mouseenter/mouseleave 事件替代 mouseover/mouseout,因其不冒泡、天然避免子元素干扰,且只在真正跨容器边界时触发;若需兼容旧 IE 或获取来源/去向节点,再用 relatedTarget 配合 contains() 判断是否真正进出。

直接用 event.relatedTarget 判断鼠标是否真正“跨边界”进出目标元素,能有效避免子元素移动引发的误触发。关键不是监听本身,而是过滤掉那些只是在内部游走、并未真正离开或进入容器的事件。
明确 relatedTarget 在 mouseover/mouseout 中的实际含义
它不是“随便哪个相邻元素”,而是有明确方向性的:
-
mouseover 触发时:
e.relatedTarget是鼠标刚刚离开的那个元素(即“来处”); -
mouseout 触发时:
e.relatedTarget是鼠标即将进入的那个元素(即“去处”)。
注意:它可能为 null(比如鼠标从地址栏、桌面或 iframe 外直接移入页面),使用前必须做存在性判断。
用 contains() 排除“假离开”和“假进入”
当鼠标在父容器内从一个子元素移到另一个子元素时,relatedTarget 往往是它的某个子节点——这并不算真正的“离开容器”。需用 target.contains(relatedTarget) 来识别并忽略这类情况:
- 对
mouseover:若relatedTarget是当前target的子元素或自身,说明鼠标没从外部来,跳过处理; - 对
mouseout:若relatedTarget是当前target的子元素或自身,说明鼠标还没真正出去,也跳过。
兼容写法(支持老 IE):
function contains(parent, child) {
return parent !== child && (parent.contains ? parent.contains(child) : !!(parent.compareDocumentPosition(child) & 16));
}
封装一个可靠的 hover 入场/出场检测函数
把逻辑收拢成可复用的判断函数,让业务代码更干净:
function isTrulyEntering(target, e) {
const rel = e.relatedTarget || e.fromElement;
return rel && !contains(target, rel) && rel !== target;
}
function isTrulyLeaving(target, e) {
const rel = e.relatedTarget || e.toElement;
return rel && !contains(target, rel) && rel !== target;
}
// 使用示例
const box = document.getElementById('box');
box.addEventListener('mouseover', e => {
if (isTrulyEntering(box, e)) {
console.log('真正从外部进入 box');
}
});
box.addEventListener('mouseout', e => {
if (isTrulyLeaving(box, e)) {
console.log('真正从 box 离开');
}
});
更轻量的替代方案:优先用 mouseenter/mouseleave
如果只需“进入容器”和“离开容器”各触发一次,且不要求知道来源/去向节点,mouseenter 和 mouseleave 是原生最优解:
- 它们不冒泡,天然避开子元素干扰;
- 只在真正跨容器边界时触发;
- 同样支持
relatedTarget(mouseenter中是离开的元素,mouseleave中是将进入的元素)。
现代浏览器全覆盖,无需兼容旧 IE 时,应优先选用。


















