React函数式定时器组件内存泄漏修复及多用户同步需求
计时会话组件完整解决方案
解决内存泄漏、页面卸载数据上报、精准计时、多页同步问题
1. 修复useCallback内存泄漏与挂载/卸载逻辑
核心是用useRef存储定时器ID和最新状态,避免闭包捕获过时值,同时在组件卸载时强制清理定时器:
import { useState, useEffect, useRef, useCallback } from 'react'; function SessionTimer() { const [isRunning, setIsRunning] = useState(false); const [elapsedTime, setElapsedTime] = useState(0); // 存储定时器ID,确保卸载时能清理 const timerRef = useRef(null); // 存储最新计时值,避免useCallback闭包问题 const latestElapsedRef = useRef(elapsedTime); // 同步最新计时到ref useEffect(() => { latestElapsedRef.current = elapsedTime; }, [elapsedTime]); // 计时tick函数,空依赖数组避免频繁重建 const tick = useCallback(() => { setElapsedTime(prev => prev + 1); }, []); // 启停与卸载清理逻辑 useEffect(() => { if (isRunning) { timerRef.current = setInterval(tick, 1000); } else { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } } // 组件卸载时强制清理定时器 return () => { if (timerRef.current) clearInterval(timerRef.current); }; }, [isRunning, tick]); // 启停按钮示例 const toggleTimer = useCallback(() => { setIsRunning(prev => !prev); }, []); return ( <div> <p>已计时:{elapsedTime}秒</p> <button onClick={toggleTimer}>{isRunning ? '暂停' : '启动'}</button> </div> ); }
关键说明:
- 用
timerRef存储定时器ID,确保组件卸载时能精准清理,杜绝内存泄漏; latestElapsedRef同步最新计时值,避免useCallback捕获过时的elapsedTime闭包;useEffect依赖仅包含必要的isRunning和tick,减少不必要的重渲染。
2. 页面关闭/跳转前上报最新计时数据
使用navigator.sendBeacon替代传统异步请求,确保页面卸载时数据能被可靠发送(浏览器会优先完成该请求):
useEffect(() => { const handleUnload = () => { // 仅当有有效计时数据时上报 if (latestElapsedRef.current > 0) { const payload = new FormData(); payload.append('sessionId', 'USER_SESSION_ID'); payload.append('elapsedSeconds', latestElapsedRef.current.toString()); // 用sendBeacon确保数据上报完成 navigator.sendBeacon('/api/save-session-duration', payload); } }; window.addEventListener('beforeunload', handleUnload); // 组件卸载时移除事件监听 return () => window.removeEventListener('beforeunload', handleUnload); }, []);
关键说明:
sendBeacon是专门为页面卸载场景设计的API,不会被页面终止打断,兼容性良好;- 用
latestElapsedRef获取最新计时值,避免闭包捕获过时数据; - 必须移除
beforeunload事件监听,防止内存泄漏。
3. 精准统计会话时长
放弃setInterval累计计时的方式,改用时间戳差值计算,规避setInterval因页面后台节流、JS线程阻塞导致的误差:
// 新增ref存储启动时间和累计暂停时长 const startTimeRef = useRef(null); const totalPausedElapsedRef = useRef(0); const startTimer = useCallback(() => { if (!isRunning) { startTimeRef.current = Date.now(); setIsRunning(true); } }, [isRunning]); const pauseTimer = useCallback(() => { if (isRunning) { // 累计暂停前的有效时长 totalPausedElapsedRef.current += Date.now() - startTimeRef.current; setIsRunning(false); startTimeRef.current = null; } }, [isRunning]); // 修改tick函数为计算真实时间差 const tick = useCallback(() => { if (startTimeRef.current) { const currentElapsed = totalPausedElapsedRef.current + (Date.now() - startTimeRef.current); // 转成秒数 setElapsedTime(Math.floor(currentElapsed / 1000)); } }, []);
关键说明:
- 用
Date.now()(或精度更高的performance.now())计算真实时间差,不受setInterval延迟影响; totalPausedElapsedRef存储暂停阶段的累计时长,确保启停切换时计时连续准确;- 即使页面被后台挂起,恢复后仍能计算出准确的离线时长。
4. 多用户/页面间定时器同步
利用localStorage的storage事件实现跨页面状态同步(若需跨用户同步,需结合WebSocket后端推送):
// 初始化时从localStorage读取同步状态 useEffect(() => { const storedState = localStorage.getItem('sessionTimerSync'); if (storedState) { const { isRunning, elapsedTime, totalPausedElapsed, startTime } = JSON.parse(storedState); setIsRunning(isRunning); setElapsedTime(elapsedTime); totalPausedElapsedRef.current = totalPausedElapsed; startTimeRef.current = startTime ? parseInt(startTime) : null; } }, []); // 状态变化时同步到localStorage useEffect(() => { const syncState = { isRunning, elapsedTime, totalPausedElapsed: totalPausedElapsedRef.current, startTime: startTimeRef.current?.toString() || null }; localStorage.setItem('sessionTimerSync', JSON.stringify(syncState)); }, [isRunning, elapsedTime]); // 监听其他页面的localStorage变化 useEffect(() => { const handleStorageUpdate = (e) => { if (e.key === 'sessionTimerSync') { const newState = JSON.parse(e.newValue); setIsRunning(newState.isRunning); setElapsedTime(newState.elapsedTime); totalPausedElapsedRef.current = newState.totalPausedElapsed; startTimeRef.current = newState.startTime ? parseInt(newState.startTime) : null; } }; window.addEventListener('storage', handleStorageUpdate); return () => window.removeEventListener('storage', handleStorageUpdate); }, []);
关键说明:
storage事件会在其他页面修改localStorage时触发当前页面的回调,实现跨页同步;- 所有状态(启停、计时、累计时长、启动时间)都需同步到
localStorage,确保新打开的页面能继承当前状态; - 若需跨用户同步,需将状态存储到后端,通过WebSocket推送状态变化到所有在线客户端。
额外注意事项
- 跨域场景下
localStorage无法同步,需改用WebSocket或Server-Sent Events实现跨页/跨用户同步; - 精准计时可替换
Date.now()为performance.now(),提供微秒级精度; - 所有添加的全局事件监听(
beforeunload、storage)必须在组件卸载时移除,杜绝内存泄漏。
内容的提问来源于stack exchange,提问作者The Old County
相关产品推荐
相关产品推荐

