React中如何向setInterval函数传入最新状态变量
问题根源
你遇到的是React Hooks场景下典型的陈旧闭包问题:setInterval创建的回调函数会永久持有首次创建时所在渲染周期的所有状态变量快照,后续yPos、dragging、scrolling这些状态更新触发组件重渲染时,已经创建的定时器回调拿不到最新的状态值,永远读的是第一次创建时的旧值。
另外你现有代码还有两个隐性bug:
- 用
scrolling这个state判断是否已经创建定时器,因为state更新是异步的,mousemove高频触发时会重复创建多个定时器,导致滚动速度越来越快 - 定时器间隔设为1000ms(1秒)滚动一次,实际体验会非常卡顿
解决方案(最小改造成本)
用useRef存储需要在定时器中访问的最新值、定时器ID,ref的current属性是可变引用,不会被闭包捕获成固定快照,任何地方访问都能拿到最新值。
第一步:新增ref做状态同步
const menuContainer = useRef(); // 新增ref存跨渲染周期需要访问的可变值 const scrollTimerRef = useRef(null); const latestYPosRef = useRef(null); const latestDraggingRef = useRef({state: false, index: null, y: null, height: null}); const [yPos, setYPos] = useState(null); const [dragging, setDragging] = useState({ state: false, index: null, y: null, height: null, }); // 每次状态更新时,同步最新值到ref useEffect(() => { latestYPosRef.current = yPos; }, [yPos]); useEffect(() => { latestDraggingRef.current = dragging; }, [dragging]); // 组件卸载时清理定时器和事件监听,避免内存泄漏 useEffect(() => { return () => { if (scrollTimerRef.current) clearInterval(scrollTimerRef.current); document.onmousemove = null; }; }, []);
第二步:改造滚动逻辑
去掉冗余的scrolling state,直接通过判断scrollTimerRef.current是否存在确认当前是否在滚动,避免重复创建定时器:
function continueScrolling() { const currentY = latestYPosRef.current; const currentDragState = latestDraggingRef.current; const containerRect = menuContainer.current.getBoundingClientRect(); const bottomThreshold = containerRect.bottom - 100; // 不满足滚动条件时立刻停止定时器 if (!currentDragState.state || currentY <= bottomThreshold) { clearInterval(scrollTimerRef.current); scrollTimerRef.current = null; return; } // 满足条件时执行滚动,步长可自行调整 menuContainer.current.scrollBy(0, 10); } const handleScroll = (draggingYPos, isDragging) => { const containerRect = menuContainer.current.getBoundingClientRect(); const bottomThreshold = containerRect.bottom - 100; const isInBottomZone = draggingYPos > bottomThreshold; // 进入底部阈值、正在拖拽、当前无运行的滚动定时器:启动滚动 if (isInBottomZone && isDragging && !scrollTimerRef.current) { // 间隔设为16ms对应约60帧,滚动更流畅,不要用1000ms scrollTimerRef.current = setInterval(continueScrolling, 16); } // 离开阈值区域/停止拖拽:立刻停止滚动 if ((!isInBottomZone || !isDragging) && scrollTimerRef.current) { clearInterval(scrollTimerRef.current); scrollTimerRef.current = null; } }
第三步:原有mousemove逻辑无需改动
document.onmousemove = (e) => { if (dragging.state) { var newYPos = window.event.clientY; setYPos(newYPos); handleScroll(newYPos, dragging.state); } }
可选优化
如果要做更丝滑的滚动体验,可以把固定步长改成动态值:根据光标距离容器边界的距离计算滚动速度,离边界越近滚动越慢,离边界越远滚动越快,和主流系统的拖拽自动滚动体验对齐。
顶部自动滚动的逻辑和底部完全一致,只要判断光标是否进入容器顶部100px阈值范围,调用scrollBy(0, -步长)向上滚动即可。
内容的提问来源于stack exchange,提问作者Jacob
相关产品推荐
相关产品推荐

