面试官:前端倒计时有误差怎么解决

前言

去年遇到的一个问题,也是非常经典的面试题了。能聊的东西还蛮多的

倒计时为啥不准

一个最简单的常用倒计时:

js 复制代码
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'时去修正时间,删掉旧的计时器,设置正确的计时,计算下一次触发的差值,然后创建新的计时器。

js 复制代码
  // 监听页面切换
  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

修改回调函数,自带修正逻辑,每次执行时都去修正

ini 复制代码
    // 每次都修正倒计时
    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 无修正

三种方式来使倒计时更准确

相关推荐
JELEE.34 分钟前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl3 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫4 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友4 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理6 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻6 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
mapbar_front7 小时前
在职场生存中如何做个不好惹的人
前端
牧杉-惊蛰7 小时前
纯flex布局来写瀑布流
前端·javascript·css
一袋米扛几楼988 小时前
【软件安全】什么是XSS(Cross-Site Scripting,跨站脚本)?
前端·安全·xss
向上的车轮8 小时前
Actix Web适合什么类型的Web应用?可以部署 Java 或 .NET 的应用程序?
java·前端·rust·.net