跳到主要内容

鼠标进出与移动委托

mouseover/mouseout 与 mouseenter/mouseleave 有什么区别?​

对比项mouseover/mouseoutmouseenter/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;
};