React中通过useEffect实现每3秒自动执行滚动函数的方法
React循环滚动列表定时器触发异常修复
问题背景
开发支持自动往复滚动的列表组件,预期滚动逻辑:先正向滚动至列表末尾,再反向滚动回列表起点,循环往复。
核心状态与基础滚动函数代码如下:
const [clicks, setClicks] = useState(numOfClicks) const [goingUp, setGoingUp] = useState(true) // 注意原代码片段未贴出current的state定义,需自行补充 const [current, setCurrent] = useState(0) const scroll = () => { if (goingUp) { elementsRef.current[current + 1].current.scrollIntoView(); setCurrent(current + 1); setClicks(clicks - 1); } else { elementsRef.current[current - 1].current.scrollIntoView(); setCurrent(current - 1); setClicks(clicks - 1); } }
滚动至方向端点时反转滚动方向的逻辑:
useEffect(() => { if (clicks === 0) { setGoingUp(!goingUp); setClicks(numOfClicks); } }, [clicks])
以上逻辑绑定点击事件触发时可正常运行,新增定时器实现固定间隔自动滚动的代码如下:
useEffect(() => { const interval = setInterval(() => { scroll() }, TIMER); return () => clearInterval(interval); }, [])
此时出现异常:在定时器回调中加console.log可确认定时器本身持续正常运行,但scroll函数仅在组件初始化时执行一次,后续不会触发滚动效果。
问题根因
这是React Hooks非常典型的过时闭包问题:
- 传入空依赖数组的
useEffect仅会在组件首次挂载时执行一次,此时注册到setInterval的回调,捕获的是组件首次渲染时的全量上下文 - 回调中调用的
scroll函数是首次渲染时生成的旧版本,其内部闭包缓存的goingUp/current/clicks全为初始状态值,后续组件状态更新时,这个旧函数永远拿不到最新的状态值 - 首次执行scroll后,后续定时器触发时调用的旧版scroll逻辑判断完全失效,甚至可能因为current值与实际DOM不匹配触发报错,表现出来就是scroll没有正常执行。
修复方案
通过ref存储最新的scroll函数引用,保证定时器永远能拿到绑定了最新状态的scroll版本,同时所有state更新改用函数式写法,彻底规避闭包旧值问题:
// 新增ref存储最新的scroll函数 const scrollRef = useRef() const scroll = () => { // 所有状态更新使用函数式写法,直接从参数拿最新状态,不依赖闭包缓存值 setCurrent(prevCurrent => { setGoingUp(prevGoingUp => { setClicks(prevClicks => { if (prevGoingUp) { elementsRef.current[prevCurrent + 1].current.scrollIntoView() } else { elementsRef.current[prevCurrent - 1].current.scrollIntoView() } return prevClicks - 1 }) return prevGoingUp }) return prevGoingUp ? prevCurrent + 1 : prevCurrent - 1 }) } // 每次组件渲染后,将最新的scroll函数存入ref useEffect(() => { scrollRef.current = scroll }) // 定时器固定调用ref中存储的最新版scroll,不需要随状态变化重建 useEffect(() => { const interval = setInterval(() => { scrollRef.current() }, TIMER); return () => clearInterval(interval); }, [])
同时修复方向反转逻辑中的闭包问题,将状态更新改为函数式写法:
useEffect(() => { if (clicks === 0) { setGoingUp(prev => !prev); setClicks(numOfClicks); } }, [clicks])
修改完成后自动滚动即可正常循环运行,且不需要频繁销毁重建定时器,没有额外性能损耗。
内容的提问来源于stack exchange,提问作者Ben_Sven_Ten
相关产品推荐
相关产品推荐

