React JS如何通过事件处理器管理interval修复番茄钟计时异常
React番茄钟计时器计数异常修复
问题根因
这个异常是多个逻辑缺陷共同导致的:
decreaseTimer中直接读取外层作用域的seconds状态做分支判断,setInterval绑定的回调会捕获首次渲染时的seconds初始值0,每次触发都会走秒数归0的分支,把秒数重设为59,这就是计时器卡在24:059的核心原因。你之前删掉switch直接用函数式更新setSeconds(prev => prev -1)能正常跑,就是因为函数式更新可以拿到最新的状态值,不受闭包旧值影响。- 点击Play按钮时没有做重复校验,每点一次就会新建一个interval实例,多个计时器同时修改状态,就会出现数值反复横跳、点一次才跳一秒的现象。
- 用state存储intervalId本身就不合理,组件重渲染时很容易拿到旧的id值,导致
clearInterval失效,清不掉已有的计时器。 - 原有秒数格式化逻辑有缺陷,秒数大于9时仍会前置补0,会出现10秒显示成
010的问题。
修复方案
- 把存储intervalId的state换成
useRef,ref的更新不会触发组件重渲染,且可以随时读取到最新的interval实例id,避免闭包导致的清计时器失效问题。 - 所有计时器内的状态更新全部使用函数式写法,不要在interval回调里直接读取外层state做判断,从根源上规避闭包旧值问题。
- 启动计时器前先清空已存在的interval实例,防止重复点击Play生成多个并行计时器。
- 修正秒数补零的格式化逻辑,新增组件卸载时的计时器清理逻辑,避免内存泄漏。
注意:父组件中对应分钟数的setter可以直接传给
onTimerMinuteChange,不要在外层包裹读取旧state的自定义回调,否则分钟更新也会触发闭包问题。
修复后完整代码
import { useEffect, useState, useRef } from "react"; const PomoTimer = (props) => { const [seconds, setSeconds] = useState(0); // 用ref存储interval id const intervalRef = useRef(null); const [isSession, setSession] = useState(true); const startTimer = () => { // 启动前先清空已有计时器,防止重复创建 if (intervalRef.current) clearInterval(intervalRef.current); intervalRef.current = setInterval(decreaseTimer, 1000); }; const stopTimer = () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; const decreaseTimer = () => { setSeconds(prevSeconds => { if (prevSeconds === 0) { // 秒数归0时分钟-1,秒数回到59 props.onTimerMinuteChange(prevMinute => prevMinute - 1); return 59; } return prevSeconds - 1; }); }; const resetTimer = () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } props.resetTimer(); props.onPlayChange(false); setSeconds(0); }; // 组件卸载时自动清理计时器,防止内存泄漏 useEffect(() => { return () => { if (intervalRef.current) clearInterval(intervalRef.current); } }, []); // 秒数补零格式化 const formatSeconds = seconds < 10 ? `0${seconds}` : seconds; return ( <section> <section> {/* <h4>{isSession ? "Session" : "Break"}</h4> */} <span>{props.timerMinute}</span> <span>:</span> <span>{formatSeconds}</span> </section> <section> <button onClick={startTimer}>Play</button> <button onClick={stopTimer}>Stop</button> <button onClick={resetTimer}>Refresh</button> </section> </section> ); }; export default PomoTimer;
内容的提问来源于stack exchange,提问作者calghir
相关产品推荐
相关产品推荐

