当前位置: 首页 > 工具软件 > real-interval > 使用案例 >

解决setInterval和setTimeout越来越慢的问题

翟善
2023-12-01

情景:后端返回一个时间差,前端将其格式化为xx时xx分xx秒后展示在页面上,然后每隔1s刷新一下倒计时。写的时候发现有不少问题,遂研究记录一下。

一、将时间差转化为xx时xx分xx秒

export const getFormatTime = (durTime: number) => {
  let remain = durTime;
  const hour = Math.floor(remain / (60 * 60 * 1000));
  remain = remain % 3600000;
  const minute = Math.floor(remain / (60 * 1000));
  remain = remain % 60000;
  const second = Math.floor(remain / 1000);
  return {
    hour,
    minute,
    second,
  };
};

这里很简单就不赘述了。

二、每隔1s刷新一下倒计时

方案1、使用 setInterval

// 假设剩余时间为1小时1分10秒
let time = 3670000;

setInterval(() => {
  time = time - 1000;
  const data = getFormatTime(time);
  // 在这更新页面数据
}, 1000)

但我印象中setInterval是有问题的,就去了解了一下,有如下几个问题:

1、setInterval会无视回调函数里的错误,而不会中止。

let count = 1;
setInterval(function () {
  count++;
  console.log(count);
  if (count % 3 === 0) throw new Error('setInterval报错');
}, 1000)

这会让一些bug难以被排查出来。

2、setInterval会在任何情况下定时执行,即使我们不希望如此。比如每隔3s轮询一次,但是用户网络很差可能隔了十几秒才拿到上一次请求的结果,这就会导致请求积压。

3、setInterval内部代码的执行时间大于延迟时间时,当次回调会被跳过。

let startTime = new Date().getTime();
setInterval(() => {
  while (new Date().getTime() < startTime + 2000) {}
  startTime = new Date().getTime()
  console.log(startTime);
}, 1000);

执行上面代码会发现,我们定的1s执行一次,实际2s才会打印一次。该问题是由setInterval的作用机制导致的,setInterval会在延迟时间过后将回调push到任务队列排队执行,但如果任务队列中已有该任务则不会push。

方案2、使用 setTimeout

let time = 3670000;

const fn = () => {
  setTimeout(() => {
    time = time - 1000;
    const data = getFormatTime(time);
    console.log(data); // 拿到data更新页面数据
    fn() // 新开定时器
  }, 1000)
}

fn()

但由于setTimeout是最小延迟时间,这样写会导致误差延迟越来越大,倒计时越来越不准。这就需要每次设置的延迟时间动态调整,以达到消除误差的目的,代码如下:

let time = 3670000;
const startTime = new Date().getTime();
let count = 0;

const fn = (nextTime) => {
  setTimeout(() => {
    time = time - 1000;
    const data = getFormatTime(time);
    console.log(data); // 拿到data更新页面数据

    count++
    const offset = new Date().getTime() - (startTime + count * 1000); // 误差时间
    fn(1000 - offset)
  }, nextTime)
}

fn(1000)

看起来没有什么问题,但实际上不论是setInterval还是setTimeout都存在一个问题,当页面被切换到后台的时候,由于浏览器的优化策略,定时器的执行会被延迟,在大约5分钟后延迟到1分钟左右执行一次。

以下方案均是在这两个方案的基础上,解决了存在的问题。

方案3、监听页面切换

let totalTime = 3670000; // 总时间
let leftTime = totalTime // 剩余时间

let startTime = new Date().getTime(); // 计时器的开始时间
let count = 0; // 计时器的执行次数

let timer = 0; //  定时器id
const fn = (nextTime) => {
  timer = setTimeout(() => {
    leftTime = leftTime - 1000;
    const data = getFormatTime(leftTime);
    console.log(data); // 拿到data更新页面数据

    count++
    const offset = new Date().getTime() - (startTime + count * 1000); // 误差时间
    fn(1000 - offset)
  }, nextTime)
}

fn(1000)

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    clearTimeout(timer)
  } else {
    totalTime = totalTime - (new Date().getTime() - startTime)
    if (totalTime < 0) {
      return;
    }
    leftTime = totalTime
    startTime = new Date().getTime()
    count = 0
    fn(1000)
  }
})

该方案通过监听页面的可见性变化,在hidden时清除定时器,在visible的时候更新总时间并重新开启定时器,以达到清除误差的作用。

方案4、real-interval

首先安装real-interval依赖:

yarn add real-interval

代码如下:

const time = 3670000;

new Interval((pass) => {
  if (time - pass * 1000 < 0) {
    return false;
  }
  const data = getFormatTime(time - pass * 1000);
  console.log(data);

}, 1000);

其中pass表示已经过了多少个1000ms,return false表示中止定时器。这种方案的优点是代码简单,缺点是会引入新的库。

方案5、Web Worker

Web Worker是运行在后台的JavaScript,不会影响页面的性能。浏览器的优化策略不会影响worker脚本,也就是说在worker脚本里的定时器一定会定期执行。

主线程:

import worker_script from './work';

// 创建worker线程,并监听消息
const worker = new Worker(worker_script); 
worker.onmessage = (event) => { 
  console.log('主线程接收到了:' + event.data);
 };

worker线程:

const workercode = () => {
  const getFormatTime = (durTime: number) => {
    let remain = durTime;
    const hour = Math.floor(remain / (60 * 60 * 1000));
    remain = remain % 3600000;
    const minute = Math.floor(remain / (60 * 1000));
    remain = remain % 60000;
    const second = Math.floor(remain / 1000);
    return {
      hour,
      minute,
      second,
    };
  };

  let time = 3670000;

  setInterval(() => {
    time = time - 1000;
    if (time < 0) {
      close(); // 关闭worker线程
      return;
    }
    const data = getFormatTime(time);
    postMessage(JSON.stringify(data)); // 向主线程发送消息
  }, 1000);
};

let code = workercode.toString();
code = code.substring(code.indexOf('{') + 1, code.lastIndexOf('}'));

const blob = new Blob([code], { type: 'application/javascript' });
const worker_script = URL.createObjectURL(blob);

export default worker_script;

这种方案代码较多,但也没有啥学习成本,很容易理解。感兴趣的同学可以看一下这篇文章:web worker 使用教程

三、总结

上面给出的5种方案其实在特定的场景下都是可以使用的,具体问题具体分析。就我个人而言,还是更喜欢方案3和方案5,这两种方案在无需引入额外依赖的情况下,较为完美的解决了定时器的各种问题。

 类似资料: