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

React Hooks疑问:滚动时如何正确设置lastScroll及计算scrollSpeed?

解析自定义Hook useDocScroll的状态更新问题与修复方案

我来帮你拆解这几个问题的核心原因,都是React状态更新机制和闭包特性导致的,咱们一步步理清楚:


核心根源:过时闭包 + React状态更新的异步性

你的useEffect依赖数组是空的[],这意味着handleScroll函数只会在组件挂载时创建一次。它内部捕获的docScroll、lastScroll都是组件初始渲染时的状态值——之后无论状态怎么更新,这个函数里的变量都不会同步最新值,这就是过时闭包问题。同时,React的状态更新是异步批量处理的,在同一个事件回调里,你无法立即拿到刚更新的状态值。


逐个解答你的疑问

1. 为什么setLastScroll(getScroll())能“正常”保存上一次滚动值?

其实这是一种巧合的表现:
你直接用getScroll()从DOM获取实时滚动值,绕过了闭包里过时的状态变量。第一次滚动时,docScroll和lastScroll都会被设为当前滚动值;第二次滚动时,组件已经因为第一次状态更新重新渲染,此时docScroll是上一次的滚动值,而getScroll()获取的是最新值,所以lastScroll会被设为最新值,看起来像是“保存了上一次”——但本质上这是依赖组件重新渲染后的状态同步,不是正确的逻辑。

2. 为什么setLastScroll(docScroll)时,lastScroll始终为0?

因为handleScroll是在useEffect([])里创建的,它捕获的docScroll是页面刚加载时的初始值(比如0)。之后无论docScroll怎么更新,这个函数里的docScroll永远都是初始的0,所以每次执行setLastScroll(docScroll)都是把lastScroll设为0。

3. 为什么setScrollSpeed(Math.abs(docScroll - lastScroll))无效?

同样是过时闭包的锅:handleScroll里的docScroll和lastScroll都是初始值(比如0和null),所以计算出来的scrollSpeed永远是0或者NaN,根本不会随着滚动更新。


修复方案:用useRef跟踪状态,避免闭包问题

要正确实现“当前滚动值、上一次滚动值、滚动速度”的逻辑,最可靠的方式是用useRef存储上一次的滚动值——因为ref的.current属性是可变的,不受闭包限制,也不会触发组件重新渲染。

修复后的完整代码:

function useDocScroll() { 
  const isClient = typeof window === "object";

  // 获取当前滚动值的工具函数
  const getScroll = () => {
    return isClient ? window.pageYOffset || document.documentElement.scrollTop : 0;
  };

  // 状态变量:当前滚动值、上一次滚动值、滚动速度
  const [docScroll, setDocScroll] = useState(getScroll());
  const [lastScroll, setLastScroll] = useState(getScroll());
  const [scrollSpeed, setScrollSpeed] = useState(0);

  // 用ref存储上一次滚动值,避免闭包问题
  const lastScrollRef = useRef(getScroll());

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

    const handleScroll = () => {
      const currentScroll = getScroll();
      // 计算滚动速度:当前值与上一次值的差的绝对值
      const speed = Math.abs(currentScroll - lastScrollRef.current);
      
      // 更新状态
      setDocScroll(currentScroll);
      setLastScroll(lastScrollRef.current);
      setScrollSpeed(speed);
      
      // 更新ref的当前值,供下一次滚动时使用
      lastScrollRef.current = currentScroll;
    };

    window.addEventListener("scroll", handleScroll);
    // 组件卸载时移除监听,避免内存泄漏
    return () => window.removeEventListener("scroll", handleScroll);
  }, []); // 依赖为空数组即可,因为用的是ref,不受闭包影响

  return [docScroll, lastScroll, scrollSpeed];
}

修复逻辑说明:

  1. 用useRef存储上一次的滚动值,每次滚动时直接读取.current属性获取最新的历史值。
  2. 在handleScroll里,先获取当前滚动值,再计算速度,最后同步更新三个状态变量。
  3. 最后更新lastScrollRef.current为当前值,确保下一次滚动时能拿到正确的历史值。

总结

你的核心问题都是源于对React闭包特性和状态更新异步性的不熟悉:

  • 不要在依赖为空的useEffect回调中直接使用状态变量,它们会永远停留在初始值。
  • 如果需要跟踪状态的历史值,useRef是比状态变量更可靠的选择,因为它不受闭包限制。
  • 状态更新是异步的,不要在同一个事件回调里依赖刚更新的状态值做计算。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 09:10:45