為什么會出現定時器不准呢?
這個就得從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