React中handleScroll函数内State无法正常工作的原因是什么?
问题分析与解决方案
这是函数组件特有的闭包陷阱导致的:
你在首次渲染的useEffect(空依赖数组[])里绑定了handleScroll,这个函数会捕获当时的scrollPos、height、top——也就是它们的初始值(都是0)。后续组件重新渲染时,虽然state已经更新,但旧的handleScroll依然拿着初始值计算,所以滚动时用的都是第一次的数值。而页面<p>标签里的{height}能正常显示,是因为每次setHeight触发重新渲染,新的渲染周期里height是最新值,JSX会直接使用这个最新值。
类组件没有这个问题,是因为类组件的this.state是可变对象,每次访问this.state.scrollPos都是取最新的属性值,不存在闭包捕获旧值的情况。
下面是几种可行的解决方案:
方案1:用useRef保存最新state值
把需要在滚动回调里访问的state同步到ref中,这样回调里能直接拿到最新值:
const [scrollPos, setScrollPos] = useState(0); const [height, setHeight] = useState(0); const [top, setTop] = useState(0); const ref = useRef(null); // 用ref存储最新的state const scrollPosRef = useRef(scrollPos); const heightRef = useRef(height); const topRef = useRef(top); // 每次state更新时同步ref useEffect(() => { scrollPosRef.current = scrollPos; heightRef.current = height; topRef.current = top; }, [scrollPos, height, top]); useEffect(() => { // 初始化height和scrollPos setHeight(ref?.current?.clientHeight); setScrollPos(-window.pageYOffset); const handleScroll = () => { const x = -window.pageYOffset; const dif = x - scrollPosRef.current; // 用最新的state值计算top const newTop = topRef.current + dif; setTop(newTop > 0 ? 0 : newTop < -heightRef.current ? -heightRef.current : newTop); setScrollPos(x); }; window.addEventListener("scroll", handleScroll); // 组件卸载时清理监听器 return () => window.removeEventListener("scroll", handleScroll); }, []);
方案2:使用函数式更新获取最新state
React的setState支持传入函数,函数参数是当前最新的state值,这样可以避开闭包捕获旧值的问题:
const [scrollPos, setScrollPos] = useState(0); const [height, setHeight] = useState(0); const [top, setTop] = useState(0); const ref = useRef(null); useEffect(() => { setHeight(ref?.current?.clientHeight); setScrollPos(-window.pageYOffset); const handleScroll = () => { const x = -window.pageYOffset; // 用函数式更新获取最新的top setTop((prevTop) => { const dif = x - scrollPos; const newTop = prevTop + dif; return newTop > 0 ? 0 : newTop < -height ? -height : newTop; }); setScrollPos(x); }; window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); }, [height, scrollPos]);
方案3:让useEffect依赖state,每次更新重新绑定监听器
把scrollPos、height、top加入useEffect的依赖数组,每次state变化时移除旧监听器、绑定新的handleScroll,这样新回调会捕获最新的state:
const [scrollPos, setScrollPos] = useState(0); const [height, setHeight] = useState(0); const [top, setTop] = useState(0); const ref = useRef(null); // 初始化height和scrollPos useEffect(() => { setHeight(ref?.current?.clientHeight); setScrollPos(-window.pageYOffset); }, []); // 每次state变化时重新绑定滚动监听器 useEffect(() => { const handleScroll = () => { const x = -window.pageYOffset; const dif = x - scrollPos; setTop(top + dif > 0 ? 0 : top + dif < -height ? -height : top + dif); setScrollPos(x); }; window.addEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll); }, [scrollPos, height, top]);
内容的提问来源于stack exchange,提问作者donCiuarin
相关产品推荐
相关产品推荐

