React中useRef存储的setInterval在useCallback内调用clearInterval无法终止
React秒表组件定时器无法终止问题解决方案
问题根因
- 开启新定时器前未清理旧实例:每次触发
handleTimerRun时直接覆盖increment.current的值,之前的定时器ID丢失无法被清除,导致多个定时器并行运行 - 不必要的依赖项触发重复执行:
handleTimerRun的依赖数组包含timerStartTime,每次timerStartTime更新都会生成新的handleTimerRun引用,触发useEffect重新执行,再次创建新的定时器 - 缺少组件卸载时的清理逻辑:组件卸载时没有主动清除定时器,会造成内存泄漏和残留逻辑执行
- 定时器闭包捕获旧值:定时器逻辑捕获了首次创建时的
timerStartTime闭包值,无法拿到最新的状态判断逻辑
修复方案
- 开启定时器前先清理已有实例,避免多定时器并存
- 新增ref同步
timerStartTime的最新值,解决闭包捕获旧值问题,同时移除handleTimerRun依赖数组中不必要的timerStartTime,避免函数频繁重建 - 给useEffect添加清理函数,在依赖变更、组件卸载时主动清理定时器
- 清除定时器后将
increment.current置为null,避免重复清理无效ID
关键代码修改
import React, { useEffect, useRef, useCallback, useContext } from 'react' import Container from '@material-ui/core/Container' import Typography from '@material-ui/core/Typography' import useLocalStorage from '../../hooks/useLocalStorage' import { FlowTimeContext } from '../../services/flow/flow-time.context' import { useStyles } from './grid-stopwatch.styles' export default function GridStopwatch() { const classes = useStyles() const { currentActivityOn } = useContext(FlowTimeContext) const [timerStartTime, setTimerStartTime] = useLocalStorage('timerStartTime', '') // 新增ref同步最新的timerStartTime,解决闭包问题 const timerStartTimeRef = useRef(timerStartTime) const [timer, setTimer] = useLocalStorage('timer', 0) const increment = useRef(null) // 同步state到ref useEffect(() => { timerStartTimeRef.current = timerStartTime }, [timerStartTime]) const handleTimerRun = useCallback(() => { // 开启新定时器前先清理旧实例 if (increment.current) { clearInterval(increment.current) } increment.current = setInterval(() => { // 从ref取最新值,不需要捕获闭包中的timerStartTime if(timerStartTimeRef.current !== ''){ const new_timer = ((Math.floor(new Date().getTime() / 1000)) - timerStartTimeRef.current) setTimer(new_timer) } else { console.log("Triggered") } }, 1000) // 移除timerStartTime依赖,避免函数频繁重建 }, [setTimer]) const handleReset = useCallback(() => { if (increment.current) { clearInterval(increment.current) // 清完后置空,避免重复清理无效ID increment.current = null } setTimer(0) setTimerStartTime('') }, [setTimer, setTimerStartTime]) const handleStart = useCallback(() => { if(timerStartTime === ''){ var start = Math.floor(new Date().getTime() / 1000) setTimerStartTime(start) } }, [timerStartTime, setTimerStartTime]) const formatTime = () => { const getSeconds = `0${timer % 60}`.slice(-2) const minutes = `${Math.floor(timer / 60)}` const getMinutes = `0${minutes % 60}`.slice(-2) const getHours = `0${Math.floor(timer / 3600)}`.slice(-2) return `${getHours}h : ${getMinutes}m : ${getSeconds}s` } useEffect(() => { if(currentActivityOn === true){ handleStart() handleTimerRun() } else { handleReset() } // 新增清理函数,依赖变更、组件卸载时自动清定时器 return () => { if (increment.current) { clearInterval(increment.current) } } }, [currentActivityOn, handleStart, handleReset, handleTimerRun]) return ( <Container maxWidth='xl' className={classes.container}> {currentActivityOn && ( <Typography variant='subtitle2' gutterBottom> 当前活动持续时间:{formatTime()} </Typography> )} </Container> ) }
修改后每次只会存在一个定时器实例,切换currentActivityOn为false时所有定时器都会被清除,不会再出现每秒多次打印、定时器无法终止的问题。
内容的提问来源于stack exchange,提问作者Ben in CA
相关产品推荐
相关产品推荐

