React.js中useEffect后台不执行,最小化浏览器后秒表暂停如何解决?
问题根因
浏览器为了降低非活跃标签页的性能消耗,会对后台页面的setInterval、setTimeout定时器做节流处理,甚至直接暂停执行。你当前的秒表实现逻辑是依赖每10ms触发一次定时器累加毫秒数,一旦定时器被暂停,计数就会中断,就会出现最小化后计时停止的问题。
另外你当前的代码还有逻辑错误:秒、分的进位判断写在了useEffect的顶层作用域,没有放在定时器回调中,本身的进位逻辑也会出现触发时机不对的问题。
修复方案
放弃基于定时器累加计数的实现,改用时间戳差值计算的方案,只要系统时间正确,不管定时器是否被节流,最终计算出的计时结果都是准确的:
- 启动时记录当前的起始时间戳,以及暂停时已经累计的计时总时长
- 每次定时器触发时,用「当前时间戳 - 起始时间戳 + 累计历史时长」得到总计时长,再换算为分、秒、毫秒
修正后完整代码
import { useState, useEffect, useRef } from "react"; export const SW = () => { const [totalTime, setTotalTime] = useState(0); // 总计时长,单位毫秒 const [isOn, setIsOn] = useState(false); const startTimeRef = useRef(0); // 存储每次启动的起始时间戳 const timerRef = useRef(null); // 存储定时器id const lastPauseTimeRef = useRef(0); // 存储暂停时累计的总时长 const start = () => { setIsOn(true); startTimeRef.current = Date.now(); }; const stop = () => { setIsOn(false); lastPauseTimeRef.current = totalTime; clearInterval(timerRef.current); }; const reset = () => { setIsOn(false); setTotalTime(0); lastPauseTimeRef.current = 0; clearInterval(timerRef.current); }; useEffect(() => { if (isOn) { timerRef.current = setInterval(() => { const currentOffset = Date.now() - startTimeRef.current; setTotalTime(lastPauseTimeRef.current + currentOffset); }, 10); } return () => clearInterval(timerRef.current); }, [isOn]); // 把总时长转成分、秒、毫秒 const min = Math.floor(totalTime / 60000); const sec = Math.floor((totalTime % 60000) / 1000); const mSec = Math.floor((totalTime % 1000) / 10); return ( <div> <p> {min.toString().padStart(2, "0")}:{sec.toString().padStart(2, "0")}: {mSec.toString().padStart(2, "0")} </p> {!isOn && <button onClick={start}>{!totalTime ? "start" : "resume"}</button>} {isOn && <button onClick={stop}>stop</button>} <button disabled={!totalTime} onClick={reset}> reset </button> </div> ); };
补充说明
- 用
useRef存储定时器、时间戳等值,可以避免频繁触发组件重渲染,也不需要把这些值放到useEffect的依赖项中 - 只要用户的系统时间没有手动修改,就算浏览器把后台定时器暂停几分钟,重新打开页面后,第一次执行定时器回调计算出的总时长依然是正确的,不会出现计时丢失的问题
内容的提问来源于stack exchange,提问作者Muhammad Rifqi
相关产品推荐
相关产品推荐

