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

前言

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

倒计时为啥不准

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

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 无修正

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

相关推荐
To_OC4 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
To_OC4 小时前
LC 17 电话号码的字母组合:我的回溯算法,就是从这道题开窍的
javascript·算法·leetcode
vipbic4 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy4 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志5 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble6 小时前
c端系统,其实更像一个信息展示平台
前端
你怎么知道我是队长6 小时前
JavaScript的变量和数据类型介绍
开发语言·javascript·ecmascript
李姆斯7 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜7 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司8 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能