如何让useEffect在useRef存储的变量变更时触发执行?
问题分析
你当前的代码存在两个核心问题:
scrollPosition是普通变量,组件每次重渲染都会重新初始化它为0,所以handleScroll里赋值后,只要组件一渲染,这个值就会被重置,根本存不住滚动位置。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
相关产品推荐
相关产品推荐

