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

如何让useEffect在useRef存储的变量变更时触发执行?

问题分析

你当前的代码存在两个核心问题:

  1. scrollPosition是普通变量,组件每次重渲染都会重新初始化它为0,所以handleScroll里赋值后,只要组件一渲染,这个值就会被重置,根本存不住滚动位置。
  2. useLayoutEffect依赖scrollPosition,但scrollPosition不是React状态变量,它的变化不会触发组件重渲染,所以useLayoutEffect完全不会执行;就算组件意外重渲染,scrollPosition也会变回0,依赖对比后也不会触发useLayoutEffect。
解决方案

针对你的视差滚动需求,这里提供两种可行的修改方式:

方案一:用useState存储滚动位置(适合需要触发组件重渲染的场景)

把scrollPosition改成React状态变量,这样它的变化会触发组件重渲染,useLayoutEffect就能检测到依赖变化并执行:

import { useRef, useEffect, useLayoutEffect, useState, useCallback } from "react";
import "./styles.css";

// 节流函数,避免scroll事件触发太频繁导致性能问题
const throttle = (func, delay) => {
  let lastCall = 0;
  return (...args) => {
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      func(...args);
    }
  };
};

export default function App() {
  const divOffset = useRef(0);
  const [scrollPosition, setScrollPosition] = useState(0);

  useEffect(() => {
    const throttledHandleScroll = throttle(handleScroll, 16); // 约60fps
    window.addEventListener("scroll", throttledHandleScroll);
    return () => {
      window.removeEventListener("scroll", throttledHandleScroll);
    };
  }, []);

  useLayoutEffect(() => {
    divOffset.current = linearInterpolation(
      divOffset.current,
      scrollPosition,
      0.07
    );
    divOffset.current = Math.floor(divOffset.current * 100) / 100;
    document.querySelector("#main").style.transform = `translateX(-${divOffset.current}px)`;
  }, [scrollPosition]);

  const handleScroll = useCallback(() => {
    setScrollPosition(window.scrollY);
  }, []);

  function linearInterpolation(x1, x2, easingValue) {
    return (1 - easingValue) * x1 + easingValue * x2;
  }

  return (
    <div id="main">
      <div className="layer1" />
      <div className="layer2" />
      <div className="layer3" />
    </div>
  );
}

方案二:用useRef存储滚动位置+直接在scroll事件里执行逻辑(性能更优,无需触发重渲染)

如果不需要组件因滚动位置变化而重渲染,直接在handleScroll里用requestAnimationFrame执行插值和样式更新,避免不必要的组件渲染:

import { useRef, useEffect } from "react";
import "./styles.css";

export default function App() {
  const divOffset = useRef(0);
  const scrollPosition = useRef(0);
  const mainDiv = useRef(null); // 用ref获取元素,避免重复querySelector

  useEffect(() => {
    const handleScroll = () => {
      scrollPosition.current = window.scrollY;
      requestAnimationFrame(updateParallax);
    };

    const updateParallax = () => {
      divOffset.current = linearInterpolation(
        divOffset.current,
        scrollPosition.current,
        0.07
      );
      divOffset.current = Math.floor(divOffset.current * 100) / 100;
      if (mainDiv.current) {
        mainDiv.current.style.transform = `translateX(-${divOffset.current}px)`;
      }
    };

    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, []);

  function linearInterpolation(x1, x2, easingValue) {
    return (1 - easingValue) * x1 + easingValue * x2;
  }

  return (
    <div id="main" ref={mainDiv}>
      <div className="layer1" />
      <div className="layer2" />
      <div className="layer3" />
    </div>
  );
}
关键说明
  • 方案一适合需要基于滚动位置更新组件UI(比如显示滚动数值)的场景,状态变化会触发组件重渲染;
  • 方案二更适合纯视差动画这类不需要组件重渲染的场景,用requestAnimationFrame能保证动画的流畅性,性能更好;
  • 两种方案都解决了“变量在重渲染时丢失”和“ref变化无法触发effect”的问题,你可以根据实际需求选择。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 16:15:05