
文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载鼠标在元素之间移动时会触发一组看似简单、实则充满细节的事件mouseover、mouseout、mousemove、mouseenter与mouseleave。本文以现代 JavaScript 教程en.javascript.info中「Moving the mouse: mouseover/out, mouseenter/leave」章节为主体结合本仓库中的可运行示例.view目录与配套练习源码系统讲解这些事件的触发时机、relatedTarget语义、快速移动导致元素被跳过的性能特性、移入子元素引发的mouseout陷阱以及如何用mouseover/mouseout实现mouseenter/mouseleave无法做到的事件委托。读完后你将掌握高亮、悬浮提示、悬停意图检测等常见交互的可靠实现方案。事件概览鼠标在元素间移动时的五类事件当鼠标指针在页面上移动时浏览器会派发以下核心事件事件触发时机是否冒泡是否区分子元素过渡mousemove指针在元素上移动是—mouseover指针进入元素是否移入子元素也算离开父元素mouseout指针离开元素是否mouseenter指针整体进入元素否是忽略到子元素的过渡mouseleave指针整体离开元素否是其中mouseover/mouseout与mouseenter/mouseleave是本篇的主角前者更底层、更灵活、支持事件委托但需要处理relatedTarget与子元素过渡带来的伪离开后者更简单直观却不冒泡、无法委托。下面逐一深入。mouseover/mouseout 与 relatedTargettarget的互补属性mouseover在鼠标指针进入元素时触发mouseout在指针离开元素时触发。这两类事件有一个专属属性relatedTarget它与target互补共同描述从哪来、到哪去的完整移动过程对于mouseoverevent.target—— 指针进入的那个元素event.relatedTarget—— 指针离开的那个元素即relatedTarget - target。对于mouseout则相反event.target—— 指针离开的那个元素event.relatedTarget—— 指针现在移向的新元素即target - relatedTarget。本仓库的 mouseoverout.view 示例在页面每个脸及其五官元素上都挂了监听并在文本域中实时打印每个事件的target与relatedTarget。其 script.js 的核心逻辑如下container.onmouseover container.onmouseout handler; function handler(event) { function str(el) { if (!el) return null; return el.className || el.tagName; } log.value event.type : target str(event.target) , relatedTarget str(event.relatedTarget) \n; // ... if (event.type mouseover) { event.target.style.background pink; } if (event.type mouseout) { event.target.style.background ; } }可以看到示例源码把target与relatedTarget成对打印同时在mouseover时把目标元素标成粉色、mouseout时清除直观展示了离开旧元素、进入新元素的完整链路。注意relatedTarget可能为nullrelatedTarget并不总是存在它可能是null。这完全正常通常意味着鼠标不是从另一个元素移来而是从窗口外部或浏览器之外直接进入或者鼠标直接离开了窗口。因此在代码中使用event.relatedTarget时必须考虑这种情况——如果直接访问event.relatedTarget.tagName当relatedTarget为null时会抛出错误。安全的做法是先做空值判断例如let from event.relatedTarget; let fromTag from ? from.tagName : (from outside the window);快速移动时浏览器会跳过中间元素mousemove会在鼠标移动时触发但这并不意味着每移动一个像素就触发一次事件。浏览器是周期性检查鼠标位置、发现位置变化后才派发事件的。因此当访问者快速划过页面时路径上的部分 DOM 元素可能被跳过如上图所示如果鼠标从#FROM高速移动到#TO中间的div元素或其中一部分可能根本不会产生任何事件——可能只触发#FROM上的mouseout紧接着触发#TO上的mouseover。这种跳过对性能是好事中间元素可能成百上千我们并不想逐个处理进入/离开每个元素的开销。但另一方面要记住指针并不会依次拜访沿途的所有元素它可能直接跳跃。尤其需要注意的是指针可能直接从窗口外部跳进页面正中间。此时relatedTarget为null因为它来自虚无仓库中的 mouseoverout-fast.view 是一个现场实验台HTML 中包含嵌套的div idchild位于div idparent内部。快速划过时可能只有 child div 触发事件也可能是 parent 触发甚至完全没有事件把指针快速移入 child 再快速向下穿过 parent 移出时如果速度足够快parent 元素会被直接忽略——鼠标从它上面跨过去而不自知。其 script.js 用一个去重计数器把相同消息合并为x N的格式便于观察大量重复事件let parent document.getElementById(parent); parent.onmouseover parent.onmouseout parent.onmousemove handler; function handler(event) { let type event.type; while (type.length 11) type ; log(type target event.target.id); return false; }一个确定的事实触发了mouseover就一定会触发mouseout在快速移动时中间元素可能被忽略但有一件事是确定的如果指针正式进入了一个元素即产生了mouseover事件那么离开它时一定会得到对应的mouseout。这一保证可以作为我们实现逻辑的可靠前提。mouseout 的陷阱移入子元素也会触发mouseout的一个重要特性是当指针从某元素移入它的后代元素时也会触发mouseout。例如对于下面的 HTMLdiv idparent div idchild.../div /div当我们位于#parent上再把指针移入更深的#child时#parent上会触发mouseout这看起来奇怪但解释很简单根据浏览器逻辑鼠标光标在任意时刻只能悬停在一个元素之上——即最深层嵌套、且 z-index 最高的那个元素。所以指针一旦去往另一个元素哪怕是后代就等于离开了前一个元素。这里还有一个事件处理的细节后代上的mouseover会向上冒泡。因此如果#parent挂了mouseover监听它也会被触发仓库中的 mouseoverout-child.view 演示了这一现象div idchild位于div idparent内#parent上挂有输出事件详情的mouseover/out处理器。当鼠标从#parent移到#child时#parent上会依次出现两个事件mouseout [target: parent]—— 离开了父元素mouseover [target: child]—— 进入子元素冒泡上来的。其 script.js 会把带时间戳的[target: xxx]事件逐条追加到文本域function mouselog(event) { let d new Date(); text.value ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n; text.scrollTop text.scrollHeight; }也就是说当指针从#parent移到#child时父元素上的两个处理器都会触发parent.onmouseout function(event) { /* event.target: parent element */ }; parent.onmouseover function(event) { /* event.target: child element (bubbled) */ };如果在处理器内不检查event.target看起来就好像指针先离开了#parent、又立刻回到了它上面。但事实并非如此——指针始终悬停在 parent 上只是移得更深、进入了 child。如果我们在离开父元素时要执行某些动作例如在parent.onmouseout里启动一段动画通常并不希望在指针只是深入#parent内部时触发。有两种规避方式在处理器中检查relatedTarget如果relatedTarget仍在当前元素内部就忽略该事件改用接下来要讲的mouseenter/mouseleave事件它们天然没有这个问题。mouseenter/mouseleave忽略子元素内部过渡且不冒泡mouseenter/mouseleave与mouseover/mouseout很相似在指针进入/离开元素时触发。但有两点重要区别元素内部的过渡不算数指针在元素与其后代之间来回移动时不会触发mouseenter/leavemouseenter/leave不冒泡。这两类事件极其简单指针进入元素无论进入点在其内部还是后代的哪个位置就触发mouseenter指针离开整个元素即离开其所有后代才触发mouseleave。仓库中的 mouseleave.view 演示了这一区别顶层元素挂的是mouseenter/mouseleave而非mouseover/mouseout。可以看到只有指针整体进出顶层元素时才产生事件指针在 child 与顶层元素之间往返、或后代之间切换时什么都不发生——后代间的过渡被完全忽略。其 script.js 与 child 版几乎一致只是监听的是mouseenter/mouseleavefunction mouselog(event) { let d new Date(); text.value ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()} | ${event.type} [target: ${event.target.id}]\n; text.scrollTop text.scrollHeight; }对比可见同样的日志代码、不同的两组事件输出截然不同——这正是理解两者差异的最佳方式。事件委托用 mouseover/mouseout 实现单元格进入/离开mouseenter/leave简单易用但不冒泡因此无法用于事件委托。设想我们想为表格的数百个单元格分别处理进入/离开。最自然的方案是把处理器挂到table上统一处理但mouseenter/leave不冒泡——如果事件发生在td上只有该td自身的处理器能捕获到它。挂在table上的mouseenter/leave处理器只会在指针整体进入/离开表格时触发完全无法得知表格内部各单元格的过渡信息。所以要支持事件委托只能使用mouseover/mouseout。第一步朴素实现——高亮指针下的元素先写一个最简单的处理器高亮鼠标当前悬停的元素。// 高亮指针下方的元素 table.onmouseover function(event) { let target event.target; target.style.background pink; }; table.onmouseout function(event) { let target event.target; target.style.background ; };仓库中的 mouseenter-mouseleave-delegation.view 就是这一版本鼠标在表格元素间移动时当前元素被高亮。其 script.js 同时在文本域中记录over - TD/out - TD之类的日志table.onmouseover function(event) { let target event.target; target.style.background pink; text.value over - ${target.tagName}\n; text.scrollTop text.scrollHeight; }; table.onmouseout function(event) { let target event.target; target.style.background ; text.value out - ${target.tagName}\n; text.scrollTop text.scrollHeight; };但我们的真实诉求是进入/离开某个td单元格整体单元格内部的过渡如在td的文本与strong之间移动、以及不在任何单元格内的过渡都不应算作进入/离开。朴素实现显然不满足——它会对每一次mouseover/out都改变高亮。第二步过滤无关事件只关心整体进入/离开 td思路如下用一个变量记作currentElem记住当前被高亮的td在mouseover时如果currentElem已存在说明还停留在上一个td内部忽略该事件在mouseout时如果没有离开当前td如移入其内部后代忽略该事件。仓库中的 mouseenter-mouseleave-delegation-2.view 提供了一个处理所有可能情况的完整实现其 script.js 如下注释为仓库原版注释// 当前鼠标下方的 td如果有 let currentElem null; table.onmouseover function(event) { // 进入新元素前鼠标总会先离开前一个元素 // 如果 currentElem 已设置说明没有离开上一个 td // 这是 td 内部的 mouseover忽略 if (currentElem) return; let target event.target.closest(td); // 没有移入 td —— 忽略 if (!target) return; // 移入了 td但在我们的表格之外可能出现于嵌套表格 // 忽略 if (!table.contains(target)) return; // 太好了我们进入了一个新的 td currentElem target; onEnter(currentElem); }; table.onmouseout function(event) { // 如果当前不在任何 td 内忽略该事件 // 这可能是表格内、但 td 之外的移动例如从一个 tr 到另一个 tr if (!currentElem) return; // 我们正在离开元素 —— 移向哪里可能是移向后代 let relatedTarget event.relatedTarget; while (relatedTarget) { // 沿父链向上检查如果仍在 currentElem 内部 // 那就是内部过渡 —— 忽略 if (relatedTarget currentElem) return; relatedTarget relatedTarget.parentNode; } // 我们真的离开了 td onLeave(currentElem); currentElem null; }; // 处理进入/离开元素的各种函数 function onEnter(elem) { elem.style.background pink; text.value over - ${currentElem.tagName}.${currentElem.className}\n; text.scrollTop 1e6; } function onLeave(elem) { elem.style.background ; text.value out - ${elem.tagName}.${elem.className}\n; text.scrollTop 1e6; }这段代码的关键特性基于事件委托处理器只挂在table上通过event.target.closest(td)定位事件实际发生的单元格从而处理表格内任意td的进入/离开。这依赖能冒泡的mouseover/out而不是不冒泡、无法委托的mouseenter/leave。过滤多余事件mouseover时若currentElem非空则忽略还在同一个td内部通过closest(td)与table.contains(target)双重检查排除非td目标与嵌套表格等边界情况mouseout时沿relatedTarget的父链向上查找若仍位于currentElem内部relatedTarget currentElem说明只是td内部过渡忽略。因此onEnter/onLeave只在指针整体进入或离开某个td时运行——快速慢速移动都一样只有td整体会被高亮与朴素的逐元素高亮版本形成鲜明对比。对应的 index.html 使用一张八卦图Bagua表格作为演示载体包含td.nw、td.n、td.c等多个带 class 的单元格配合textarea日志与清空按钮便于观察高亮与事件记录的联动。实战扩展基于移动事件的组件理解了上述机制就可以组装更高级的交互组件。本章节目录下的两个练习正好展示了两个典型应用。悬浮提示tooltip处理嵌套元素练习 1-behavior-nested-tooltip 要求为带data-tooltip属性的元素显示悬浮提示且元素可以嵌套同一时刻只显示最深层的一个提示。例如div>class HoverIntent { constructor({ sensitivity 0.1, // 速度低于 0.1px/ms 视为悬停在元素上 interval 100, // 每 100ms 测量一次鼠标速度 elem, over, out }) { // ... elem.addEventListener(mouseover, this.onMouseOver); elem.addEventListener(mouseout, this.onMouseOut); } // ... trackSpeed() { let speed; if (!this.lastTime || this.lastTime this.prevTime) { speed 0; // 光标没动 } else { speed Math.sqrt( Math.pow(this.prevX - this.lastX, 2) Math.pow(this.prevY - this.lastY, 2) ) / (this.lastTime - this.prevTime); } if (speed this.sensitivity) { clearInterval(this.checkSpeedInterval); this.isHover true; this.over.call(this.elem); } else { // 速度快把新坐标记为前一次坐标继续测量 this.prevX this.lastX; this.prevY this.lastY; this.prevTime this.lastTime; } } }从源码可以看到实现要点mouseover进入元素后立即记录初始坐标prevX/prevY与时间prevTime并启动mousemove监听与setInterval(trackSpeed, interval)定时测速每次mousemove只记录最新坐标与时间lastX/lastY/lastTime真正的速度判定放在trackSpeed中用欧氏距离除以时间间隔得到速度px/ms低于sensitivity默认 0.1即判定为悬停并回调overmouseout时通过event.relatedTarget与elem.contains()判断是否真正离开了元素若relatedTarget为null离开窗口或不在元素内才停止测速必要时回调out提供destroy()方法统一移除所有监听便于清理。使用示例来自练习描述let tooltip document.createElement(div); tooltip.className tooltip; tooltip.innerHTML Tooltip; new HoverIntent({ elem, over() { tooltip.style.left elem.getBoundingClientRect().left px; tooltip.style.top elem.getBoundingClientRect().bottom 5 px; document.body.append(tooltip); }, out() { tooltip.remove(); } });效果是快速划过元素时无反应慢速移入或停留时显示提示且光标在元素子元素之间移动时提示不会闪烁因为mouseout处理器用relatedTarget过滤了内部过渡。总结本文覆盖了mouseover、mouseout、mousemove、mouseenter与mouseleave五类鼠标移动事件需要记住的关键点快速移动会跳过中间元素浏览器周期性检查鼠标位置指针可能跳跃过部分 DOM 元素这有利于性能但若触发了mouseover则离开时必有mouseout。relatedTarget是target的互补属性mouseover/out与mouseenter/leave都有它表示来处/去处它可能为null从窗口外进入或离开窗口使用时务必判空。mouseover/out在父子元素间也会触发浏览器假定指针任意时刻只能悬停在一个最深层的元素上所以从父元素移入子元素时父元素会收到mouseout同时子元素的mouseover会冒泡——处理时要检查event.target与relatedTarget以过滤伪离开。mouseenter/leave更简单只在整个元素进出时触发忽略内部过渡且不冒泡——因此无法用于事件委托。事件委托请用mouseover/mouseout配合closest()、contains()与沿relatedTarget父链遍历等技术可以精确实现整体进入/离开td这类需求参考 mouseenter-mouseleave-delegation-2.view/script.js。所有演示均可直接在仓库对应.view目录中打开 HTML 运行体验想挑战自己的理解可以动手完成 1-behavior-nested-tooltip 与 2-hoverintent 两个配套练习。赞分享文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载相关推荐ZLMediaKit流媒体实时回看技术架构3种实现方案对比与深度架构解析ZLMediaKit流媒体实时回看技术架构3种实现方案对比与深度架构解析 在视频监控、在线教育、直播平台等实时性要求极高的场景中视频录制后的实时回看功能已成音视频直播后端现代 JavaScript 教程事件冒泡、捕获与事件委托一文讲清 JavaScript 事件机制现代 JavaScript 教程事件冒泡、捕获与事件委托一文讲清 JavaScript 事件机制 《现代 JavaScript 教程》The Modern文档教程前端泰语NLP开发者必看gpt2-base-thai模型架构与代码实现原理详解泰语NLP开发者必看gpt2 base thai模型架构与代码实现原理详解 对于从事泰语自然语言处理的开发者来说gpt2 base thai模型是一个重要的上一篇PHPStan 错误 identifier constant.value 完全解析dynamicConstantNames 类型校验与修复指南下一篇Gel Cloud 应用部署到 Fly.io 全指南运行时与构建期 Secrets 配置详解创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考