为什么会出现定时器不准呢?
这个就得从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/