React.js用setInterval实现带暂停/恢复的计数器,渲染数值不更新
代码核心问题定位
- 非响应式变量失效:代码中通过
var声明的elapsed、secondsLeft属于组件渲染作用域内的临时变量,每次组件重新渲染都会被重置为初始值20;同时修改这类普通变量不会触发React重渲染,即便计数器逻辑正常运行,页面上的数值也不会同步更新。 - 计时器重复创建:直接在组件函数体中调用
timer()的写法,会在每一次组件渲染时都新建一个setInterval,多个计时器叠加运行,不仅逻辑混乱还会造成内存泄漏。 - 闭包值过期:
setInterval的回调会捕获创建时的作用域变量,拿到的isPlay、secondsLeft永远是首次创建时的初始值,后续状态更新后,回调内也读取不到最新值。 - 暂停逻辑反转:当前代码中
isPlay初始为false(默认应为运行状态),但间隔回调内写的是if (isPlay === true)就暂停计数,逻辑完全颠倒。
修正后可直接运行的实现
import { useState, useEffect, useRef } from 'react'; const ProgressBar = (props) => { // 剩余秒数通过state存储,修改后自动触发重渲染 const [secondsLeft, setSecondsLeft] = useState( props.duration ? props.duration : 20 ); // 运行状态标识:true为已暂停,false为运行中 const [isPaused, setIsPaused] = useState(false); // 用ref存储计时器实例,跨渲染保持同一引用 const intervalRef = useRef(null); // 统一处理计时器的创建、清除逻辑 useEffect(() => { // 暂停状态下直接清除已有计时器 if (isPaused) { clearInterval(intervalRef.current); return; } intervalRef.current = setInterval(() => { setSecondsLeft(prev => { // 倒计时到0时清除计时器 if (prev <= 0) { clearInterval(intervalRef.current); return 0; } // 基于上一次的state值递减,避免闭包捕获旧值 return prev - 1; }) }, 1000); // 组件卸载时清除计时器,避免内存泄漏 return () => clearInterval(intervalRef.current); }, [isPaused, secondsLeft <= 0]); // 切换暂停/恢复状态 const toggleStatus = () => { setIsPaused(prev => !prev); }; return ( <> <p>剩余时间:{secondsLeft} 秒</p> <button onClick={toggleStatus}> {isPaused ? '恢复' : '暂停'} </button> </> ); }; export default ProgressBar;
实现逻辑说明
- 倒计时数值存储在
useState中,每一次递减都会触发React重渲染,保证页面展示的数字和实际计数逻辑完全同步 - 计时器实例存储在
useRef中,不会因为组件重渲染丢失引用,暂停时直接清除对应实例即可中断计数,恢复时基于当前剩余秒数重新创建计时器,实现断点续跑 - 所有副作用逻辑统一收敛在
useEffect中,根据运行状态动态调整计时器生命周期,从根源上避免闭包捕获旧状态的问题
内容的提问来源于stack exchange,提问作者Thomas James
相关产品推荐
相关产品推荐

