在后台监控页、公告栏或聊天记录展示区,经常需要让一个固定高度的容器里的列表内容自己往上滚动,并且当用户把鼠标移上去看某一条的时候,滚动要立刻停下来。用原生 JavaScript 就能干净利落地实现这个效果,不需要引第三方库。

一、实现原理概述
核心思路是:准备一个外层容器限制高度并隐藏溢出,里面放一个内容块。用 JavaScript 不断地把内容块的纵向偏移量减少几个像素,容器看起来就往上滚了。当内容块滚到最底部(也就是偏移量绝对值大于等于内容高度减容器高度)时,把偏移量重置为 0,形成循环。暂停功能则是用一个布尔变量控制动画是否继续推进,鼠标进入设成 false,离开设回 true。
过去很多人习惯用 setInterval 每 30 毫秒改一次 scrollTop,但定时器在页面切到后台时会积压,回来后容易突跳。我们改用 requestAnimationFrame,它跟着浏览器刷新走,暂停后下一帧就不累加位移,体验更顺。下面的代码以滚动内层 div 的 transform 为例,也可以直接操作容器的 scrollTop。
二、基础 HTML 结构
结构越简单越好,外层 .scroll-box 定高溢出隐藏,内层 .scroll-content 放真实列表。这样 JS 只动内层偏移,不影响布局流。
<div class="scroll-box" id="box" style="height:200px;overflow:hidden;border:1px solid #ccc;">
<div class="scroll-content" id="content">
<p>第一条消息:系统运行正常</p>
<p>第二条消息:备份任务已完成</p>
<p>第三条消息:新版本已发布</p>
<p>第四条消息:磁盘使用率 70%</p>
<p>第五条消息:安全检查通过</p>
<p>第六条消息:定时任务注册成功</p>
<p>第七条消息:用户反馈已处理</p>
</div>
</div>
上面这段里,外层高度只有 200px,而七条消息远超这个高度,所以默认只能看见前几条,剩下的要靠滚动露出来。注意所有标签都按规则做了转义,避免被浏览器当成真实节点解析。
三、JavaScript 控制逻辑
我们先定义速度、是否运行的标志、以及动画函数。动画里每次给位移加一点,到边界就归零。mouseenter 和 mouseleave 直接改 running 的值,下一帧自然就停或继续。
var box = document.getElementById('box');
var content = document.getElementById('content');
var speed = 0.5; // 每帧移动的像素
var offset = 0;
var running = true;
function step() {
if (running) {
offset -= speed;
var maxOffset = content.offsetHeight - box.offsetHeight;
if (-offset >= maxOffset) {
offset = 0; // 循环回到顶部
}
content.style.transform = 'translateY(' + offset + 'px)';
}
requestAnimationFrame(step);
}
requestAnimationFrame(step);
box.addEventListener('mouseenter', function () {
running = false;
});
box.addEventListener('mouseleave', function () {
running = true;
});
代码中的 content.offsetHeight 是内容真实高度,box.offsetHeight 是视口高度,两者之差就是最多能滚的距离。用 translateY 而不是改 top,是因为 transform 不触发重排,性能更好。如果你更喜欢原生滚动条,把 transform 那行换成 box.scrollTop = -offset 即可,判断条件里的 offset 符号相应调整。
这种写法的优点是暂停零延迟,鼠标一碰就定住;缺点是如果列表特别长且速度很慢,用户可能觉得枯燥。此时可以把 speed 提到 1 或结合 IntersectionObserver 只让可见区域滚。另外在移动端没有 mouseenter,可以监听 touchstart 和 touchend 做同样的事。
四、与 setInterval 方案的对比
为了看清差异,我们列一个小表格。实际项目里若只是简单页面,两者都能用,但涉及后台标签页或动画连贯性,requestAnimationFrame 更稳。
| 对比项 | setInterval | requestAnimationFrame |
|---|---|---|
| 后台标签页表现 | 计时器堆积,回前台跳变 | 浏览器自动降频,无跳变 |
| 暂停响应 | 需清定时器,易留间隙 | 改标志位,下一帧停 |
| 性能开销 | 独立计时,可能掉帧 | 合帧渲染,更省电 |
从表里能看出,新方案在健壮性和能耗上都占优。若团队老代码用了 setInterval,只需把内部逻辑搬到 rAF 循环,并保留 mouseenter 或 mouseover 的开关即可平滑迁移。
五、常见误区与补充
有人会把 mouseover 和 mouseenter 混用,其实 mouseover 会冒泡,子元素来回划也会反复触发,可能导致标志位抖动。用 mouseenter 和 mouseleave 绑定在外层容器上就不会有这问题。还有人忘记内容高度不足时不滚,那样 offset 一直减会变成负数空白,所以加一句 if (maxOffset <= 0) return; 在内容少时直接退出动画最安全。
最后提醒,如果列表数据是动态加载的,每次新增节点后重新算一次 maxOffset 就行,不用重启动画。把 maxOffset 的求值放到 step 内部(像上面示例那样)本来就能自适应高度变化,这是把计算放进帧循环的一个隐形好处。
JavaScript循环滚动鼠标悬停暂停修改时间:2026-08-06 07:33:36