鼠标进出与移动委托
mouseover/mouseout 与 mouseenter/mouseleave 有什么区别?
| 对比项 | mouseover/mouseout | mouseenter/mouseleave |
|---|---|---|
| 进入 / 离开子元素 | 会触发 | 忽略内部移动 |
| 是否冒泡 | 冒泡 | 不冒泡 |
relatedTarget: 与target互补的另一端;mouseover时是"从哪来",mouseout时是"去哪";- 可能为
null: 指针从窗口外进入或离开窗口, 此时读event.relatedTarget.tagName会报错; - 进入子元素时父元素上收到两个事件: 先
mouseout [target: parent], 再冒泡上来的mouseover [target: child]; - 背后模型: 指针同一时刻只在一个元素上, 即最内层且 z-index 最上的那个, 进入子元素也算离开父元素;
- 选择: 整体进入/离开时才做动画或高亮, 用
mouseenter/mouseleave; 需要委托则用mouseover/mouseout, 见 事件委托与自定义事件;
鼠标快速移动为什么会跳过元素, 委托怎么写?
- 采样: 浏览器隔一段时间才检查一次指针位置, 只在发现变化时派事件, 所以快速移动会跳过中间元素;
- 保证: 中间元素可以被跳过, 但"进入过某元素, 就一定会收到它的
mouseout"; - 动机: 中间元素可能极多, 逐个派发代价大, 跳过是性能优化而非 bug;
- 委托限制:
mouseenter/mouseleave不冒泡, 挂在<table>上只能感知整表的进出, 拿不到单元格级信息, 所以委托必须用mouseover/mouseout加过滤; - 过滤思路: 用变量记住当前高亮的单元格, "仍在其中"的进出事件一律忽略; 用
closest('td')找到单元格, 用relatedTarget沿父链上溯判断是否仍在单元格内;
let currentElem = null;
table.onmouseover = (event) => {
if (currentElem) return; // 在同一个 td 内部移动, 忽略
const target = event.target.closest("td"); // 也可能落在表格外的嵌套单元格上
if (!target || !table.contains(target)) return;
currentElem = target;
currentElem.style.background = "pink";
};
table.onmouseout = (event) => {
if (!currentElem) return;
for (let r = event.relatedTarget; r; r = r.parentNode) {
if (r === currentElem) return; // 仍留在单元格内 -> 内部转移, 忽略
}
currentElem.style.background = "";
currentElem = null;
};