解决js定时器不准的问题

为什么会出现定时器不准呢?

这个就得从js的执行机制说起了,在事件循环(EventLoop)执行机制中,异步事件(setInterval/setTimeout)会把回调函数放入消息队列(Event Queue)中,主线程的宏任务执行完毕后,依次执行消息队列中的微任务,等微任务执行完了再循环回来执行宏任务。由于消息队列中存在大量的任务,其他任务的执行时间就会造成定时器回调函数的延迟,如果不处理,就会一直叠加延迟,当运行时间久了之后,相差就会很大。

因此定时器是不能完全保证的。

解决方案

1. 动态计算时差(仅针对循环定时器起到修正作用)

在定时器开始前和在运行时动态获取当前时间戳,在设置下一次定时时长时,在期望值的基础上减去当前差值,以获取相对精确的定时器运行效果

此方法仅能消除setInterval长时间运行造成的误差,或者setTimeout循环长时间运行的累计误差,无法对当个定时器消除执行的延迟

// 每秒倒计时的实现
let startTime, // 开始时间
  count, // 计数器
  runTime, // 当前时间
  downSecond = 1200,  // 倒计时时间
  loopTimer = null;

function resetDefaultValue() {

  startTime = Date.now();
  count = 0;
  runTime = 0;
}

resetDefaultValue(); //每次倒计时执行前要重置一下初始值
loop();
function loop() {

  runTime = Date.now();
  let offsetTime = runTime - (startTime + count * 1000); //时间差
  count++;
  let nextTime = 1000 - offsetTime; //下一次定时器需要的时间
  nextTime = nextTime > 0 ? nextTime : 0;
  downSecond-- ;
  // 处理逻辑区域 ---- s
  console.log('时间差:'+offsetTime, ',下一次需要时间:'+ nextTime)
  if (downSecond <= 0) {

    // 结束定时器
    clearTimeout(loopTimer)
    loopTimer = null;
    return false;
  }
  // 处理逻辑区域 ---- e
  loopTimer = setTimeout(loop, nextTime);
}

 

 

 

var count = count2 = 0;
 var runTime,runTime2;
 var startTime,startTime2 = performance.now();//获取当前时间
 
 //普通任务-对比
 setInterval(function(){
     runTime2 = performance.now();
     ++count2;    
     console.log("普通任务",count2 + ' --- 延时:' + (runTime2 - (startTime2 + count2 * 1000)) + ' 毫秒');
 }, 1000);
 
 //动态计算时长
 function func(){
  runTime = performance.now();
     ++count;
     let time = (runTime - (startTime + count * 1000));
     console.log("优化任务",count2 + ' --- 延时:' + time +' 毫秒');
     //动态修正定时时间
     t = setTimeout(func,1000 - time);
 }
 startTime = performance.now();
 var t = setTimeout(func , 1000);
 
 //耗时任务
 setInterval(function(){
     let i = 0;
     while(++i < 100000000);
 }, 0);

 

 

 从上面看出,不管是setTimeout还是setInterval,在长时间运行中,都会存在误差,而修正就是将定时器拉会原来的轨道

2. 使用web worker

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

<html>
<meta charset="utf-8">
<body>
<script type="text/javascript">
var count = 0;
var runTime;

//performance.now()相对Date.now()精度更高,并且不会受系统程序堵塞的影响。
//API:https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/now
var startTime = performance.now(); //获取当前时间

//普通任务-对比测试
setInterval(function(){
    runTime = performance.now();
    ++count;    
    console.log("普通任务",count + ' --- 普通任务延时:' + (runTime - (startTime + 1000))+' 毫秒');
    startTime = performance.now();
}, 1000);

//耗时任务
setInterval(function(){
    let i = 0;
    while(i++ < 100000000);
}, 0);

// worker 解决方案
let worker = new Worker('worker.js');
</script>
</body>
</html>
// worker.js
var count = 0;
var runTime;
var startTime = performance.now();
setInterval(function(){
   runTime = performance.now();
   ++count;
   console.log("worker任务",count + ' --- 延时:' + (runTime - (startTime + 1000))+' 毫秒');
   startTime = performance.now();
}, 1000);

 

 可以看到使用worker后,延迟会非常小,基本上在3毫秒内,而且worker任务不受其他任务的干扰,即使浏览器进入后台,也没有影响worker

使用web worker要注意以下几点

(1)同源限制

分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。

(2)DOM 限制

Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。

(3)通信联系

Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。

(4)脚本限制

Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。

(5)文件限制

Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。

 参考:

https://johnresig.com/blog/how-javascript-timers-work/

https://blog.csdn.net/qq_41494464/article/details/99944633

http://www.ruanyifeng.com/blog/2018/07/web-worker.html 

 

posted on 2022-01-07 10:50  sjpqy  阅读(2666)  评论(0编辑  收藏  举报

导航