You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 19:45:13