React实现支持自定义时长、暂停/恢复、重置的倒计时秒表
React倒计时秒表缺陷修复方案
原有代码核心问题
原有实现无法满足需求的根因如下:
- 未存储定时器实例ID,暂停、重置操作无法清除正在运行的定时器,会出现多定时器叠加、计时速度异常、暂停后仍走时的问题
- 未单独保存用户初始输入的时间值,重置功能无参考基准
- 输入值直接绑定展示状态,秒数超过60时不会自动进位换算
- 状态更新存在闭包陷阱,定时器回调内读取的运行状态不是最新值
- RESET按钮未绑定任何交互逻辑,无法实现重置效果
修复后完整实现
import { useState, useRef, useEffect } from "react"; import "./styles.css"; export default function App() { // 用户输入的原始分、秒值 const [inputMin, setInputMin] = useState(0); const [inputSec, setInputSec] = useState(0); // 剩余总秒数(计时核心状态) const [remainSeconds, setRemainSeconds] = useState(0); // 运行状态标记 const [isRunning, setIsRunning] = useState(false); // 存储初始总秒数,用于重置 const initialTotalRef = useRef(0); // 存储定时器ID,跨渲染周期保存 const timerRef = useRef(null); // 格式化展示分、秒,自动补零 const displayMin = String(Math.floor(remainSeconds / 60)).padStart(2, '0'); const displaySec = String(remainSeconds % 60).padStart(2, '0'); // 组件卸载时清除定时器,避免内存泄漏 useEffect(() => { return () => { if (timerRef.current) clearInterval(timerRef.current); } }, []); // 启动倒计时 const handleStart = () => { if (isRunning) return; // 计算总秒数,自动处理秒数超60的进位逻辑 const total = Number(inputMin) * 60 + Number(inputSec); if (total <= 0) return; initialTotalRef.current = total; setRemainSeconds(total); setIsRunning(true); timerRef.current = setInterval(() => { setRemainSeconds(prev => { if (prev <= 1) { clearInterval(timerRef.current); setIsRunning(false); return 0; } return prev - 1; }); }, 1000); }; // 暂停/恢复切换 const handlePauseToggle = () => { if (isRunning) { // 暂停:清除定时器,更新状态 clearInterval(timerRef.current); setIsRunning(false); } else { // 恢复:没有剩余时间时不触发 if (remainSeconds <= 0) return; setIsRunning(true); timerRef.current = setInterval(() => { setRemainSeconds(prev => { if (prev <= 1) { clearInterval(timerRef.current); setIsRunning(false); return 0; } return prev - 1; }); }, 1000); } }; // 重置 const handleReset = () => { clearInterval(timerRef.current); setIsRunning(false); setRemainSeconds(initialTotalRef.current); }; return ( <div className="App"> <h1>倒计时秒表</h1> <div> <label>分钟:</label> <input type="number" min="0" value={inputMin} onChange={(e) => setInputMin(Number(e.target.value) || 0)} disabled={isRunning} /> <label>秒:</label> <input type="number" min="0" value={inputSec} onChange={(e) => setInputSec(Number(e.target.value) || 0)} disabled={isRunning} /> </div> <div style={{ margin: '20px 0', fontSize: '32px' }}> {displayMin}:{displaySec} </div> <div> <button onClick={handleStart} disabled={isRunning}>START</button> <button onClick={handlePauseToggle} disabled={remainSeconds === 0}> {isRunning ? 'PAUSE' : 'RESUME'} </button> <button onClick={handleReset}>RESET</button> </div> </div> ); }
实现说明
- 秒数超60自动进位逻辑通过总秒数换算实现,例如输入5分90秒时,总秒数为
5*60+90=390,自动换算为6分30秒开始倒计时 - 定时器ID存在
useRef中,跨渲染周期不会丢失,可随时清除避免计时错乱 - 倒计时到0时自动清除定时器、停止运行,不会出现负数时间
- 运行中禁用输入框,避免计时过程中修改输入值导致逻辑异常
- 按钮根据当前状态自动禁用无效操作,比如运行中START按钮不可点击,无有效计时时暂停/恢复按钮不可点击
内容的提问来源于stack exchange,提问作者Vivek Joshi
相关产品推荐
相关产品推荐

