
文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载在鼠标事件驱动的 Web 交互中一个经典痛点是提示框tooltip在鼠标只是路过元素时也会频繁弹出造成令人烦躁的闪烁。本篇文章基于 Modern JavaScript Tutorial 项目中的 hoverintent 任务完整拆解如何实现一个通用的HoverIntent对象它通过测量鼠标在元素上的移动速度来区分停下来了显示提示与只是经过忽略从而打造出只在用户真正停留时才出现的智能提示框。读完本文你将掌握基于mouseover/mousemove/mouseout的鼠标速度采样算法、事件去抖的思路以及如何用dispatchEvent构造自动化测试来验证交互逻辑。一、需求背景为什么普通 mouseover 不够用任务文档 task.md 给出了明确的产品诉求只有当访客把鼠标移到元素上并停下来时才显示 tooltip如果只是把鼠标快速穿过元素则不需要显示——谁想要多余的闪烁呢原文who wants extra blinking?。技术上可以这样落地测量鼠标在元素上的移动速度速度慢就认为鼠标来到了元素上并触发over速度快则直接忽略。这实质上是一种基于速度阈值的意图识别与许多编辑器/IDE 中延迟显示悬浮文档如 VS Code 的 hover的思路一致。二、HoverIntent 的公共接口设计任务要求构造一个通用对象new HoverIntent(options)其options包含三个核心回调配置项类型含义elemElement需要跟踪鼠标行为的元素overFunction鼠标来到元素上缓慢移动或停下时调用outFunction鼠标离开元素时调用前提是之前调用过over任务文档中的典型用法示例创建并定位一个 tooltip// a sample tooltip let tooltip document.createElement(div); tooltip.className tooltip; tooltip.innerHTML Tooltip; // the object will track mouse and call over/out 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(); } });除了这三个业务回调仓库实现还在构造器中提供了两个可调参数带默认值见 hoverIntent.js参数默认值含义sensitivity0.1速度低于0.1像素/毫秒即判定为悬停interval100每100ms采样一次鼠标速度三、核心算法四步走官方解答 solution.md 将算法归纳为清晰的四步在元素上挂mouseover/mouseout处理器也可用mouseenter/mouseleave但它们不够通用引入事件委托后将无法工作详见第五节鼠标进入元素后在mousemove上开始测量速度如果速度慢则执行over当鼠标离开元素时如果此前执行过over则执行out。四、关键约束JavaScript 拿不到当前鼠标坐标如何测量速度最直觉的想法是每隔100ms运行一次函数比较前后两次坐标的距离距离小则速度小。但这个方案有一个硬约束JavaScript 里没有getCurrentMouseCoordinates()这样的函数无法主动读取鼠标当前坐标。唯一的途径是监听mousemove等鼠标事件从事件对象上取坐标。因此实现策略变为在mousemove处理器中持续记录坐标和时间戳然后由setInterval每隔100ms取出最近一次记录的坐标进行对比。这正是仓库源码HoverIntent类采用的方案。五、源码逐段解析HoverIntent 类的完整实现仓库提供了两个版本的实现文件source.view中是留白待补全的骨架 source.view/hoverIntent.js供练习solution.view中是可运行的完整解答 solution.view/hoverIntent.js。下面以完整解答为准逐段讲解。5.1 构造器参数默认值与事件绑定use strict; class HoverIntent { constructor({ sensitivity 0.1, // speed less than 0.1px/ms means hovering over an element interval 100, // measure mouse speed once per 100ms elem, over, out }) { this.sensitivity sensitivity; this.interval interval; this.elem elem; this.over over; this.out out; // make sure this is the object in event handlers. this.onMouseMove this.onMouseMove.bind(this); this.onMouseOver this.onMouseOver.bind(this); this.onMouseOut this.onMouseOut.bind(this); // and in time-measuring function (called from setInterval) this.trackSpeed this.trackSpeed.bind(this); elem.addEventListener(mouseover, this.onMouseOver); elem.addEventListener(mouseout, this.onMouseOut); } // ... }两个细节值得注意解构 默认值sensitivity与interval在解构时直接赋予默认值调用方可以不传bind绑定 this事件处理器和trackSpeed由setInterval回调都需要把this锁定为类实例否则回调中this会指向元素或全局对象。源码注释明确说明了这一意图。5.2 onMouseOver进入元素开始采样onMouseOver(event) { if (this.isOverElement) { // if were over the element, then ignore the event // we are already measuring the speed return; } this.isOverElement true; // after every mousemove well be check the distance // between the previous and the current mouse coordinates // if its less than sensivity, then the speed is slow this.prevX event.pageX; this.prevY event.pageY; this.prevTime Date.now(); elem.addEventListener(mousemove, this.onMouseMove); this.checkSpeedInterval setInterval(this.trackSpeed, this.interval); }逻辑要点isOverElement标志位防止重复启动测量鼠标在子元素间移动导致mouseover冒泡重复触发时直接返回记录进入时的基准坐标prevX/prevY与基准时间prevTime挂上mousemove采样监听并启动setInterval定时计算速度。5.3 onMouseMove只记录不计算onMouseMove(event) { this.lastX event.pageX; this.lastY event.pageY; this.lastTime Date.now(); }这里体现了第四节的关键约束由于无法主动读取坐标只能被动地在每次mousemove时更新最近一次坐标。注意mousemove本身触发非常频繁所以这个处理器刻意保持轻量——采样与判定交给setInterval统一进行避免在每次移动事件里做开销较大的计算。5.4 trackSpeed定时计算速度并判定trackSpeed() { let speed; if (!this.lastTime || this.lastTime this.prevTime) { // cursor didnt move 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 { // speed fast, remember new coordinates as the previous ones this.prevX this.lastX; this.prevY this.lastY; this.prevTime this.lastTime; } }速度计算公式speed √((Δx)² (Δy)²) / Δt即两次采样点之间的欧氏距离除以时间差单位为 px/ms。判定逻辑光标未移动lastTime为空或与prevTime相同速度记为0必然小于阈值直接判定为悬停速度 sensitivity默认 0.1 px/ms判定为停留清除定时器、置isHover true并以this.elem为this调用over()速度较快不触发over但要把当前采样点滚动为上一个点prevX/prevY/prevTime保证下一轮对比的是相邻两个采样点之间的距离而非始终与进入点对比——这是算法不出错的关键细节。over.call(this.elem)意味着回调内this指向被跟踪元素方便回调直接访问元素属性。5.5 onMouseOut真正离开时才收尾onMouseOut(event) { // if left the element if (!event.relatedTarget || !elem.contains(event.relatedTarget)) { this.isOverElement false; this.elem.removeEventListener(mousemove, this.onMouseMove); clearInterval(this.checkSpeedInterval); if (this.isHover) { // if there was a stop over the element this.out.call(this.elem, event); this.isHover false; } } }这里处理了本系列文章中反复强调的子元素边界问题详见 article.md 的 Mouseout when leaving for a child 一节鼠标从父元素移入其子元素时父元素也会收到mouseout且relatedTarget指向子元素。若不加以判断鼠标在子元素间移动就会反复触发out造成闪烁。判定条件!event.relatedTarget || !elem.contains(event.relatedTarget)的含义relatedTarget为null鼠标来自/去往窗口外部——视为真正离开relatedTarget不是本元素的子孙——视为真正离开relatedTarget仍在元素内部——忽略此次mouseout继续维持测量状态。这正好实现了任务结尾强调的体验当光标在时钟的子元素之间移动时tooltip 不会闪烁。5.6 destroy()释放监听配合测试与组件卸载destroy() { elem.removeEventListener(mousemove, this.onMouseMove); elem.removeEventListener(mouseover, this.onMouseOver); elem.removeEventListener(mouseout, this.onMouseOut); }destroy()移除全部事件监听是组件可复用性的必备接口测试中每个用例结束后调用它以避免用例间污染见第七节生产环境在组件卸载时也应调用防止内存泄漏。六、为什么用 mouseover/out 而不是 mouseenter/leavesolution.md第一步特别指出虽然也可以用mouseenter/mouseleave但它们不够通用如果引入事件委托将无法工作。原因在配套主教程 article.md 的 Event delegation 一节中有完整论述mouseenter/mouseleave不冒泡无法在父容器上用事件委托统一处理成百上千个子元素例如表格的每个td而mouseover/mouseout会冒泡配合relatedTarget检查即可在委托场景下精确区分进入/离开某个元素整体。HoverIntent采用mouseover/mouseout正是为了保留委托能力这是设计上的一处前瞻性考量。七、测试验证dispatchEvent 构造鼠标事件solution.md结尾提示解答的测试通过dispatchEvent派发合成鼠标事件来验证 tooltip 行为是否正确。仓库中的测试文件 solution.view/test.js 给出了完整的验证矩阵它是理解本算法行为边界的绝佳教材测试基础设施function mouse(eventType, x, y, options) { let eventOptions Object.assign({ bubbles: true, clientX: x, clientY: y, pageX: x, pageY: y, target: elem }, options || {}); elem.dispatchEvent(new MouseEvent(eventType, eventOptions)); }该辅助函数用new MouseEvent(type, {...})构造合成事件并通过elem.dispatchEvent(...)派发同时借助 Mocha 的before/after钩子配合sinon.useFakeTimers()伪造时钟精确控制setInterval/setTimeout的推进this.clock.tick(ms)。五个关键用例及其含义用例模拟操作断言mouseover - when the pointer just arrived, no tooltip仅派发mouseover坐标 10,10尚未触发overmouseover - after a delay, the tooltip shows upmouseover后clock.tick(100)坐标不变触发over停住即悬停mouseover - followed by fast mouseout leads doesnt show tooltipmouseover后 30ms 派发远距离mouseout不触发overmouseover - slow move - tooltipsmouseover后每 10ms 移动 2pxi/5共 200ms触发over平均速度约 0.2px/10ms0.02px/ms 0.1mouseover - fast move - no tooltipmouseover后每 10ms 移动 10pxi共 200ms不触发over速度 1px/ms 0.1慢移与快移两个用例的对比精准验证了sensitivity 0.1 px/ms的阈值划分同时也演示了用合成事件驱动被测逻辑的通用测试模式——这种模式不依赖真实鼠标硬件可在 CI 中稳定复现。八、完整集成示例时钟 Demo 的组装任务页面的 demo 用一枚时钟元素展示效果快速扫过时钟无提示缓慢移动或停在上面则出现 tooltip。仓库中该 demo 的实现位于 solution.view/index.html核心结构如下div idelem classclock span classhours12/span : span classminutes30/span : span classseconds00/span /div div idtooltip hiddenTooltip/div script new HoverIntent({ elem, over() { tooltip.style.left elem.getBoundingClientRect().left 5 px; tooltip.style.top elem.getBoundingClientRect().bottom 5 px; tooltip.hidden false; }, out() { tooltip.hidden true; } }); /script与任务文档中的示例略有差异但思路一致这里 tooltip 预先存在于 DOM 中hidden属性控制显隐定位使用getBoundingClientRect()读取元素位置让提示框显示在元素左下方。页面配套样式见 solution.view/style.css#tooltip为绝对定位、高z-index、带边框阴影的浮层样式.clock中的时/分/秒分别用红/绿/蓝三色区分——这样鼠标在三个子元素之间移动时你可以直观地验证tooltip 不闪烁这一核心体验。注意由于HoverIntent内部对mouseout的relatedTarget做了elem.contains()检查鼠标从#elem移入span.hours等子元素时不会误触发out这正是第八节分析的代码价值所在。九、参数调优与适用边界sensitivity默认 0.1 px/ms阈值越小需要鼠标停得越死才触发over阈值越大则越容易触发。触控板、高分辨率显示器等场景可能需要按实际手感调整。interval默认 100ms采样间隔影响响应延迟与开销。间隔越大over判定越迟钝间隔越小越灵敏但setInterval回调更频繁。适用前提本方案依赖pageX/pageY与Date.now()计算速度适用于常规桌面端鼠标交互场景触摸屏上没有mouseover语义需要另行设计。已知边界正如主教程 article.md 所述快速移动时浏览器可能跳过中间元素Skipping elements此时relatedTarget可能为nullHoverIntent.onMouseOut已将其视为真正离开行为正确但若在over/out回调中直接访问event.relatedTarget.tagName之类的属性需先判空。小结HoverIntent是一个小而完整的实战组件浓缩了鼠标事件编程的三项核心技能速度即意图用mousemove采样 setInterval定时对比坐标差/时间差把停留与路过转化为可量化的速度阈值事件选择与边界处理坚持用可冒泡的mouseover/mouseout保留事件委托能力并用relatedTargetcontains()过滤子元素穿越产生的伪事件可测性与可复用性通过destroy()暴露清理接口配合dispatchEvent与假时钟在无真实鼠标的环境下验证全部行为分支。如果你需要在项目中实现智能悬浮提示、延迟加载预览或任何需要区分悬停与划过的交互直接复用本文剖析的HoverIntent类完整源码见 solution.view/hoverIntent.js练习骨架见 source.view/hoverIntent.js并参考配套测试 solution.view/test.js 验证你的实现即可。赞分享文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载相关推荐Apache Druid 数据删除完整指南从软删除到 kill 任务永久清除Apache Druid 数据删除完整指南从软删除到 kill 任务永久清除 本指南以 docs/data management/delete.md http文档/教程前端Radix Vue Tooltip Cursor 实战实现一个跟随鼠标光标移动的 TooltipRadix Vue Tooltip Cursor 实战实现一个跟随鼠标光标移动的 Tooltip 导读 本篇文章围绕 Radix Vue现 reka ui前端UI组件设计系统VLC鼠标点击暂停插件5分钟快速上手指南VLC鼠标点击暂停插件5分钟快速上手指南 在视频播放过程中频繁切换播放状态是许多用户的常见需求无论是学习外语时需要反复跟读还是观看教学视频时需要记录重点内音视频插件系统上一篇download4cj异步下载实战如何用enqueue和enqueues高效管理多个文件下载下一篇Apache Pulsar Solr Sink Connector 完全指南从 Topic 到 Solr 集合的消息落库实践创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考