JavaScript 中的 Mouse Events:为什么你的 UI 会闪烁(以及如何正确修复)
Source: Dev.to
悬停交互看似简单——直到它们悄悄破坏了你的 UI。
最近,在构建数据表时,每行都有一个在悬停时出现的“操作”列。大多数情况下它能正常工作,但当鼠标缓慢移动或跨越行边界时,UI 会闪烁,有时甚至会有两行同时显示操作。罪魁祸首并不是 CSS 或渲染,而是鼠标事件模型。
两类鼠标悬停事件
| Event | Bubbles | Fires when |
|---|---|---|
mouseover | Yes | 鼠标进入元素 或其任何子元素 |
mouseout | Yes | 鼠标离开元素 或其任何子元素 |
mouseenter | No | 鼠标进入元素本身 |
mouseleave | No | 鼠标离开元素本身 |
冒泡与非冒泡悬停事件之间的区别是 UI 工程中最重要的之一。
为什么 mouseover 对 UI 状态危险
<tr>
<td>Name</td>
<td>
<button>Edit</button>
<button>Delete</button>
</td>
</tr>
从用户的角度来看,在按钮之间切换时他们仍然在“悬停该行”。而从浏览器的角度来看,指针会经过:
→ →
每一次切换都会触发新的 mouseover 和 mouseout 事件,因为光标在子元素之间移动。因此:
- 从一个按钮移动到另一个按钮时,会在第一个按钮上触发
mouseout,该事件会冒泡。 - 行元素会收到一个“鼠标离开”的信号,即使用户实际上从未离开该行。
这种 DOM 移动 与 人类意图 之间的不匹配会导致闪烁。
我的表格是怎么坏掉的
- 每行在悬停时显示操作按钮。
- 行之间有 1 px 的边框。
- 当光标越过该边框时,会短暂地先离开一行再进入下一行。
产生的顺序:
mouseout→ 隐藏第一行的操作。mouseover→ 显示下一行的操作。
如果时间足够快,两个行会同时处于激活状态,导致闪烁。布局本身没有问题;是事件模型错误地表示了用户意图。
为什么 mouseenter 能解决这个问题
mouseenter 和 mouseleave 不会冒泡,仅在指针真正进入或离开元素本身时触发——而不是它的子元素。相同的移动:
→ →
只会触发一次 mouseenter(tr)(随后再触发一次 mouseleave(tr)),从而消除错误的离开事件并防止闪烁。这使它们非常适合用于:
- 表格行
- 下拉菜单
- 工具提示
- 悬停卡片
- 任何在光标保持在元素内部时都应保持激活的 UI
简而言之:
mouseenter→ 用户意图mouseover→ DOM 遍历
何时使用每种
使用 mouseenter / mouseleave 的情况:
- 基于悬停切换 UI 状态。
- 子元素不应中断悬停。
- 稳定性很重要。
示例
- 行操作
- 导航菜单
- 个人资料卡片
- 工具提示
使用 mouseover / mouseout 的情况:
- 需要了解进入或离开的子元素。
- 冒泡对委托行为有用。
示例
- 图像映射
- 每个图标的工具提示
- 对单个元素的自定义悬停效果
React 使这更微妙
在 React 中,onMouseOver 和 onMouseOut 被包装在合成事件系统中,增加了另一层传播和重新渲染。这可能放大闪烁和竞争条件,使基于悬停的 UI 更难正确实现。
实用经验法则
如果你正在使用 mouseover 来控制 UI 可见性,那么你的实现可能比较脆弱。大多数基于悬停的界面应该使用 mouseenter / mouseleave 来构建,因为用户悬停的是 事物,而不是原始的 DOM 节点。
最后思考
我表格里那短暂的闪烁并不是 bug——它提醒我们浏览器的事件模型有多么深奥。最优秀的 UI 工程师会编写符合人类实际交互方式的逻辑。通常,糟糕的 UI 与坚如磐石的 UI 之间的差别仅在于一个事件名称。