You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ReactJS实现鼠标按下或按钮悬停时的平滑连续滚动效果求助

问题根因
  • 你当前实现卡顿的核心原因是 setInterval 与 scrollBy({behavior: 'smooth'}) 的动画逻辑冲突:smooth 参数本身会触发浏览器自带的滚动过渡动画,叠加固定间隔的滚动调用会导致动画队列阻塞,出现跳帧、不连贯的问题
  • 其次 setInterval 的执行时机无法和浏览器的渲染帧对齐,无法保证60fps的流畅度
优化实现思路
  1. 用 requestAnimationFrame 替代 setInterval 实现滚动逻辑,和浏览器渲染周期对齐,保证动画流畅
  2. 移除 scrollBy 的 smooth 参数,每帧直接计算滚动增量,避免多重动画冲突
  3. 增加滚动边界判断,滚动到容器顶部/底部时自动停止,避免无效计算
  4. 如果后续要实现Etsy的悬停滚动效果,只需要额外监听容器的mouseover事件,根据鼠标在容器的Y轴坐标动态调整滚动方向和速度即可
可运行的平滑滚动示例代码
import { useRef, useState, useLayoutEffect, useEffect } from "react";
import styled from "styled-components";

const Wrapper = styled.div`
  margin: 5% 40%;
  .test {
    height: 300px;
    width: 100px;
    overflow: hidden;
    border: 1px solid #eee;
    .inner {
      background-image: linear-gradient(red, blue);
      height: 800px;
      width: 100%;
    }
  }
  button {
    margin-top: 10px;
    padding: 8px 12px;
  }
`;

// 替代setInterval的帧动画hook,和浏览器渲染对齐
function useAnimationFrame(callback, isRunning) {
  const savedCallback = useRef(callback);
  const frameIdRef = useRef();

  useLayoutEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  useEffect(() => {
    if (!isRunning) return;

    const loop = () => {
      savedCallback.current();
      frameIdRef.current = requestAnimationFrame(loop);
    };
    frameIdRef.current = requestAnimationFrame(loop);

    return () => cancelAnimationFrame(frameIdRef.current);
  }, [isRunning]);
}

const Scroll = () => {
  const scrollRef = useRef(null);
  const [isScrolling, setIsScrolling] = useState(false);
  // 可调整滚动速度,数值越大滚动越快
  const scrollStep = 2;

  useAnimationFrame(() => {
    if (!scrollRef.current) return;
    const el = scrollRef.current;
    // 滚动到边界自动停止
    if (el.scrollTop + el.clientHeight >= el.scrollHeight) return;
    el.scrollTop += scrollStep;
  }, isScrolling);

  return (
    <Wrapper>
      <div ref={scrollRef} className="test">
        <div className="inner"></div>
      </div>

      <button
        onMouseDown={() => setIsScrolling(true)}
        onMouseUp={() => setIsScrolling(false)}
        onMouseLeave={() => setIsScrolling(false)}
      >
        HOLD DOWN TO SCROLL
      </button>
    </Wrapper>
  );
};

export default Scroll;
扩展Etsy悬停滚动的补充逻辑

如果要实现悬停区域触发滚动的效果,只需要给滚动容器添加以下事件逻辑:

  • 监听mousemove事件,计算鼠标当前在容器内的Y轴位置
  • 当鼠标在容器顶部20%区域时,设置滚动步长为负数(向上滚动)
  • 当鼠标在容器底部20%区域时,设置滚动步长为正数(向下滚动)
  • 当鼠标在中间区域时,停止滚动
  • 监听mouseleave事件,离开容器时自动停止滚动

内容的提问来源于stack exchange,提问作者Lberteh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 17:54:03