记录---前端倒计时有误差怎么解决
<h1 data-id="heading-0">🧑💻 写在开头</h1><p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<h2 data-id="heading-0">前言</h2>
<blockquote>
<p>去年遇到的一个问题,也是非常经典的面试题了。能聊的东西还蛮多的</p>
</blockquote>
<h2 data-id="heading-1">倒计时为啥不准</h2>
<p>一个最简单的常用倒计时:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:bash;gutter:true;">const = useState(0)
let total = 10// 倒计时10s
const countDown = ()=>{
if(total > 0){
setCount(total)
total--
setTimeout(countDown ,1000)
}
}
</pre>
</div>
<p> </p>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202504/2149129-20250407172329524-2131226992.png" alt="" loading="lazy"></p>
<div>
<div>
<p>稍微有几毫秒的误差,但是问题不大。 原因:JavaScript是单线程,<code>setTimeout</code> 的回调函数会被放入事件队列,既然要排队,就可能被前面的任务阻塞导致延迟 。且任务本身从call stack中拿出来执行也要耗时。所以有1000变1002也合理。就算<code>setTimeout</code>的第二个参数设为0,也会有至少有4ms的延迟。</p>
<p>如果切换了浏览器tab,或者最小化了浏览器,那误差就会变得大了。</p>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202504/2149129-20250407172349460-720856456.png" alt="" loading="lazy"></p>
<p> </p>
<div>
<div>
<p>倒计时10s,实际时间却经过了15s,误差相当大了。(不失为一种穿越时间去到未来的方法)</p>
<p>原因:当页面处于后台时,浏览器会降低定时器的执行频率以节省资源,导致 <code>setTimeout</code> 的延迟增加。切回来后又正常了</p>
<p>目标:解决切换后台导致的倒计时不准问题</p>
<h3 data-id="heading-2">解决方案1</h3>
<p>监听 visibilitychange 事件,在切回tab时修正。</p>
<p>页面从后台离开或者切回来,都能触发visibilitychange事件。只需在document.visibilityState === 'visible'时去修正时间,删掉旧的计时器,设置正确的计时,计算下一次触发的差值,然后创建新的计时器。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:bash;gutter:true;">// 监听页面切换
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)
}
}
</pre>
</div>
<p> 特点:会跳过一些时刻计数,可能会错过一些关键节点上事件触发。如果长时间离开,误差变大,实际时间结束,倒计时仍在,激活页面时才结束。</p>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202504/2149129-20250407172402950-35953204.png" alt="" loading="lazy"></p>
<h3 data-id="heading-3">解决方案2</h3>
<p>修改回调函数,自带修正逻辑,每次执行时都去修正</p>
<div class="cnblogs_Highlighter">
<pre class="brush:bash;gutter:true;"> // 每次都修正倒计时
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)
}
}</pre>
</div>
结果:<br>
</div>
<div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202504/2149129-20250407172446759-153902015.png" alt="" loading="lazy"></p>
<p> </p>
<div>
<div>
<p>特性:每个倒计时时刻都触发,最后更新更精准。(顺便一提,edge浏览器后台状态timeout间隔最低是1000)</p>
<h3 data-id="heading-4">解决方案3</h3>
<p>上面的都依赖Date模块,改本地时间就会爆炸,一切都乱套了。(可以用performance.now 来缺相对值判断时间)</p>
<p>有没有方案让时钟像邓紫棋一样一直倒数的</p>
</div>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202504/2149129-20250407172502921-1542300940.png" alt="" loading="lazy"></p>
<p> 有的,就是用web worker,单独的线程去计时,不会受切tab影响</p>
<div class="cnblogs_Highlighter">
<pre class="brush:bash;gutter:true;">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);
}
}
</pre>
</div>
<p> </p>
<div class="cnblogs_Highlighter">
<pre class="brush:bash;gutter:true;">const = 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();
};
}, []);
</pre>
</div>
<p> </p>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202504/2149129-20250407172528435-295263066.png" alt="" loading="lazy"></p>
<p> </p>
<p>缺点:worker的缺点 ;优点:精准计时</p>
<h3 data-id="heading-5">总结:</h3>
<blockquote>
<p>方案1 大修正</p>
<p>方案2 小修正</p>
<p>方案3 无修正</p>
<p>三种方式来使倒计时更准确</p>
</blockquote>
<br>
</div>
</div>
<div>
<h2>本文转载于:https://juejin.cn/post/7478687361737768986</h2>
</div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
<p><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></p>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/18813199
頁:
[1]