面试官:前端倒计时有误差怎么解决
前言
去年遇到的一个问题,也是非常经典的面试题了。能聊的东西还蛮多的
倒计时为啥不准
一个最简单的常用倒计时:
const [count, setCount] = useState(0) let total = 10 // 倒计时10s const countDown = ()=>{ if(total > 0){ setCount(total) total-- setTimeout(countDown ,1000) } }
稍微有几毫秒的误差,但是问题不大。原因:JavaScript是单线程,setTimeout
的回调函数会被放入事件队列,既然要排队,就可能被前面的任务阻塞导致延迟 。且任务本身从call stack中拿出来执行也要耗时。所以有1000变1002也合理。就算setTimeout
的第二个参数设为0,也会有至少有4ms的延迟。
如果切换了浏览器tab,或者最小化了浏览器,那误差就会变得大了。
倒计时10s,实际时间却经过了15s,误差相当大了。(不失为一种穿越时间去到未来的方法)
原因:当页面处于后台时,浏览器会降低定时器的执行频率以节省资源,导致 setTimeout
的延迟增加。切回来后又正常了
目标:解决切换后台导致的倒计时不准问题
<需要看新机会的>
顺便吆喝一句,技术大厂,待遇之类的给的还可以,就是偶尔有加班(放心,加班有加班费)
前、后端/测试,多地有空位,感兴趣的可以试试机会~~
解决方案1
监听 visibilitychange 事件,在切回tab时修正。
页面从后台离开或者切回来,都能触发visibilitychange事件。只需在document.visibilityState === 'visible'时去修正时间,删掉旧的计时器,设置正确的计时,计算下一次触发的差值,然后创建新的计时器。
// 监听页面切换 useEffect(() => { const handleVisibilityChange = () => { console.log('Page is visible:', document.visibilityState); if(document.visibilityState === 'visible'){ updateCount() } }; // 添加事件监听器 document.addEventListener('visibilitychange', handleVisibilityChange); // 清理函数:移除事件监听器 return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, []); // 修正倒计时 const updateCount = ()=>{ clearTimeout(timer) // 清除 const nowStamp = Date.now() const pastTime = nowStamp - firstStamp const remainTime = CountSeconds * 1000 - pastTime if(remainTime > 0){ setCount(Math.floor(remainTime/1000)) total = Math.floor(remainTime/1000) timer = setTimeout(countDown,remainTime%1000) }else{ setCount(0) console.log('最后时间:',new Date().toLocaleString(),'总共耗时:', nowStamp-firstStamp) } }
特点:会跳过一些时刻计数,可能会错过一些关键节点上事件触发。如果长时间离开,误差变大,实际时间结束,倒计时仍在,激活页面时才结束。
解决方案2
修改回调函数,自带修正逻辑,每次执行时都去修正
// 每次都修正倒计时 const countDown = ()=>{ const nowDate = new Date() const nowStamp = nowDate.getTime() firstStamp = firstStamp || nowStamp lastStamp = lastStamp || nowStamp const nextTime = firstStamp + (CountSeconds-total) * 1000 const gap = nextTime - nowStamp ; // 如果当前时间超过了下一次应该执行的时间,就修正时间 if(gap < 1){ clearTimeout(timer) if(total == 0){ setCount(0) console.log('最后时间:',nowDate.toLocaleString(),'总共耗时:', nowStamp-firstStamp) }else{ console.log('left',total, 'time:',nowDate.toLocaleString(),'间隔:',nowStamp-lastStamp) lastStamp = nowStamp setCount(total) total-- countDown() } }else{ timer = setTimeout(countDown,gap) } }
结果:
特性:每个倒计时时刻都触发,最后更新更精准。(顺便一提,edge浏览器后台状态timeout间隔最低是1000)
解决方案3
上面的都依赖Date模块,改本地时间就会爆炸,一切都乱套了。(可以用performance.now 来缺相对值判断时间)
有没有方案让时钟像邓紫棋一样一直倒数的
有的,就是用web worker,单独的线程去计时,不会受切tab影响
ini 代码解读复制代码let intervalId; let count = 0; self.onmessage = function (event) { const data = event.data; // 接收主线程传递的数据 console.log('Worker received:', data); count = data; intervalId = setInterval(countDown,1000); // 这里用了interval }; function countDown() { count-- self.postMessage(count); // 将结果发送回主线程 if (count == 0) { clearInterval(intervalId); } }
javascript 代码解读复制代码const [worker, setWorker] = useState(null); // 初始化 Web Worker useEffect(() => { const myWorker = new Worker(new URL('./worker.js', import.meta.url)); // 监听 Worker 时钟 返回的消息 myWorker.onmessage = (event) => { // console.log('Main thread received:', event.data); const left = event.data const nowDate = new Date() const nowStamp = nowDate.getTime() if(left > 0){ const gap = nowStamp - lastStamp console.log('left',left, 'time:',nowDate.toLocaleString(),'间隔:',gap) lastStamp = nowStamp setCount(left) }else{ setCount(0) console.log('最后时间:',nowDate.toLocaleString(),'总共耗时:', nowStamp-firstStamp) } }; setWorker(myWorker); // 清理函数:关闭 Worker return () => { myWorker.terminate(); }; }, []);
缺点:worker的缺点 ;优点:精准计时
总结:
方案1 大修正
方案2 小修正
方案3 无修正
三种方式来使倒计时更准确
——转载自作者:水下黑化已放电