setInterval 不準的原因及問題解決方案
setInterval 是 JavaScript 中用于定時執(zhí)行任務(wù)的常用方法。它的基本語法如下:
const intervalId = setInterval(callback, delay, ...args);
- callback 是要執(zhí)行的函數(shù)。
- delay 是每次執(zhí)行之間的時間間隔(以毫秒為單位)。
- args 是傳遞給 callback 的附加參數(shù)。
但是,在實際使用中,可能會發(fā)現(xiàn) setInterval 并不總是精確地按照預(yù)期的間隔時間來執(zhí)行任務(wù)。這是因為 JavaScript 中的定時器并不是絕對精準的。
1. 為什么定時器不精準?
1.1 單線程執(zhí)行
JavaScript 是單線程的,這意味著所有的任務(wù)都是在一個線程上排隊執(zhí)行的。雖然通過 setInterval 設(shè)置了定時器,想要周期性地執(zhí)行某個任務(wù),但如果在回調(diào)執(zhí)行時主線程正在處理其他任務(wù)(比如執(zhí)行計算、渲染 UI、處理用戶事件等),就會導(dǎo)致回調(diào)函數(shù)被延遲執(zhí)行,從而使定時器間隔時間不準確。
舉個 ??:設(shè)置了一個定時器,每隔 100ms 執(zhí)行一次回調(diào)。
let count = 0; const startTime = Date.now(); console.log(`Start time: ${startTime} ms`); const intervalId = setInterval(() => { count++; console.log(`Callback executed: ${count}, Time: ${Date.now() - startTime} ms`); }, 100); // 這里模擬一個會占用主線程的長任務(wù) setTimeout(() => { console.log(`Long task starting... Time: ${Date.now() - startTime} ms`); let start = Date.now(); while (Date.now() - start < 500) {} // 占用 500ms 的時間 console.log(`Long task finished, Time: ${Date.now() - startTime} ms`); }, 0);
事件流和時間線分析:
t = 0ms:開始時刻,setInterval 在 100ms 后首次觸發(fā),然后執(zhí)行 setTimeout 長任務(wù)。
t = 500ms:長任務(wù)結(jié)束,主線程被釋放,但主線程的阻塞導(dǎo)致 setInterval 的回調(diào)沒有按預(yù)定時間執(zhí)行。
t = 100ms:setInterval 第一次回調(diào),但由于主線程被阻塞,回調(diào)沒有執(zhí)行。
t = 600ms:setInterval 執(zhí)行下一次回調(diào)。后面的 callback 也對應(yīng)推遲。
輸出日志:
主線程被占用時,定時器回調(diào)會被推遲,造成時間間隔的不準確。這種情況發(fā)生在主線程繁忙時,任務(wù)執(zhí)行排隊,定時器回調(diào)會“排隊”到后面,等待后續(xù)執(zhí)行。
1.2 任務(wù)排隊和事件循環(huán)
JavaScript 使用事件循環(huán)來調(diào)度任務(wù)。setInterval 的回調(diào)是被放入 宏任務(wù)隊列 中的,而宏任務(wù)隊列的執(zhí)行是有優(yōu)先級的,只有在當前執(zhí)行棧為空時,才會去執(zhí)行隊列中的任務(wù)。如果在執(zhí)行回調(diào)時有其他更優(yōu)先的任務(wù)(如 UI 渲染、用戶輸入處理等),那么定時器回調(diào)可能會被延遲。
舉個 ??
let count = 0; const startTime = Date.now(); // 設(shè)置一個定時器,每 100ms 執(zhí)行一次回調(diào) const intervalId = setInterval(() => { count++; console.log(`setInterval callback executed: ${count}`); }, 100); // 模擬高優(yōu)先級任務(wù) console.log('Start of the script execution,time:', Date.now() - startTime); // 使用 Promise 模擬一個高優(yōu)先級任務(wù) Promise.resolve().then(() => { console.log('Promise microtask started,time:', Date.now() - startTime); // 模擬一些耗時的同步操作 let start = Date.now(); while (Date.now() - start < 300) {} // 阻塞主線程 300ms console.log('Promise microtask finished,time:', Date.now() - startTime); }); // 模擬低優(yōu)先級的異步任務(wù) setTimeout(() => { console.log('setTimeout task executed,time:', Date.now() - startTime); }, 50); console.log('End of the script execution,time:', Date.now() - startTime);
微隊列優(yōu)先執(zhí)行,導(dǎo)致 setInterval 和 setTimeout 的回調(diào)被推遲。
1.3 回調(diào)執(zhí)行時間的累積誤差
setInterval 設(shè)定的間隔時間只是開始和結(jié)束之間的期望時間,但回調(diào)函數(shù)的執(zhí)行時間會影響下一個回調(diào)的執(zhí)行時間。如果回調(diào)函數(shù)本身執(zhí)行時間較長,那么定時器的間隔就會被推遲。
舉個 ??
let count = 0; const startTime = Date.now(); const intervalId = setInterval(() => { count++; console.log(`Callback executed: ${count}, time: ${Date.now() - startTime}`); // 模擬一個需要較長時間的操作 let start = Date.now(); while (Date.now() - start < 100) {} // 長時間的計算任務(wù)(100ms) }, 50);
解釋:定時器第一次觸發(fā)時,回調(diào)開始執(zhí)行,100ms 后 while 循環(huán)才結(jié)束,所以下一個回調(diào)被推遲。新的定時器回調(diào)只能在上一個回調(diào)結(jié)束后才會被執(zhí)行,后續(xù)回調(diào)也會相應(yīng)推遲。
當回調(diào)函數(shù)本身執(zhí)行占用的時間太長,它會影響到下一個回調(diào)的執(zhí)行時間,導(dǎo)致實際的回調(diào)間隔變得比預(yù)期更長。因此,定時器的間隔時間不僅僅是設(shè)定的期望時間,回調(diào)函數(shù)本身的執(zhí)行時間也會對實際間隔產(chǎn)生影響。
1.4 最小時間間隔為 4ms
為什么存在 4ms 的最小時間限制?
當嵌套 5 層以上的定時器時,瀏覽器會因為時間精度和性能原因,將最小執(zhí)行間隔限制為 4 ms。這是一種性能優(yōu)化措施,目的是防止過于頻繁的定時器調(diào)用占用過多的計算資源,導(dǎo)致頁面的響應(yīng)變慢或無響應(yīng)。這個 4 ms 的限制對于大多數(shù)常見的 Web 應(yīng)用程序來說已經(jīng)足夠使用。
舉個 ??
// 試圖設(shè)置 1ms 間隔 let count = 0; const startTime = Date.now(); const intervalId = setInterval(() => { count++; if (count === 100) { console.log('100 times executed', Date.now() - startTime); clearInterval(intervalId); } }, 1); // 這里實際上會以 4ms 或更長的間隔執(zhí)行
如果多個定時器同時設(shè)置為非常小的時間間隔(如 1ms),瀏覽器可能會把這些定時器合并成更大的間隔(如 4ms)。因此,setInterval 的間隔時間不會精確到設(shè)置的時間,尤其在高頻率執(zhí)行時。
1.5 失活頁面強制調(diào)整到 1s
這種優(yōu)化通常稱為 頁面不可見時定時器凍結(jié)。它意味著當瀏覽器頁面不處于前臺(如切換標簽頁或最小化瀏覽器窗口時),瀏覽器會自動降低定時器的執(zhí)行頻率和精度,來節(jié)省資源并提升性能。例如,setInterval 的最小間隔時間可能會從 100 毫秒變成 1 秒,甚至更長。
具體原因:在瀏覽器后臺,頁面不再主動渲染,用戶的交互也被暫停,因此瀏覽器會降低后臺頁面的計算和渲染頻率,以減少資源消耗。當頁面失活時,setInterval 的頻率會被降低,以減少對 CPU 和內(nèi)存的占用。
let count = 0; let intervalId = setInterval(() => { count++; console.log(`回調(diào) #${count} 執(zhí)行, 時間: ${new Date().toLocaleTimeString()}`); }, 100); // 設(shè)置為 100 毫秒間隔 // 在 3 秒后清除定時器 setTimeout(() => { clearInterval(intervalId); console.log("定時器已停止"); }, 3000);
2. 如何實現(xiàn)更精準的定時器?
以下是幾種常用的方法:
2.1 使用 requestAnimationFrame(針對動畫)
requestAnimationFrame 是一種專門為動畫設(shè)計的定時器方法,與 setInterval 和 setTimeout 不同,requestAnimationFrame 在瀏覽器的渲染周期內(nèi)執(zhí)行回調(diào),因此,能提供比 setInterval 更加精準的時間間隔,特別適用于動畫或視覺渲染任務(wù)。
原理:
- requestAnimationFrame 會確?;卣{(diào)函數(shù)在下一次重繪之前執(zhí)行,并且在瀏覽器處于后臺時會自動暫停,從而節(jié)省資源。
- 由于 requestAnimationFrame 是與瀏覽器的刷新率綁定的,它的執(zhí)行頻率與瀏覽器的刷新率(通常是每秒 60 次,即 16.67 毫秒)同步。因此它能在不同的瀏覽器和設(shè)備上提供更一致的執(zhí)行間隔。
舉個 ??:實現(xiàn)一個簡單的動畫,讓一個小方塊在屏幕上移動。
使用 setInterval
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>setInterval vs requestAnimationFrame</title> <style> body { margin: 0; overflow: hidden; background-color: #f0f0f0; } .box { position: absolute; width: 50px; height: 50px; background-color: red; } </style> </head> <body> <div class="box"></div> <script> const box = document.querySelector('.box'); let posX = 0; // 使用 setInterval 來創(chuàng)建動畫 setInterval(() => { posX += 2; if (posX > window.innerWidth) { posX = -50; } box.style.transform = `translateX(${posX}px)`; }, 16); // 約等于每秒 60 幀 </script> </body> </html>
示例中,我們設(shè)置了定時器每 16 ms 執(zhí)行一次回調(diào),這大約是每秒 60 次(60 FPS)。但是由于 setInterval 是基于固定時間間隔的,它不能保證每次回調(diào)執(zhí)行時都能與瀏覽器的渲染周期同步。如果回調(diào)執(zhí)行得太慢,或者頁面有其他任務(wù)需要處理,定時器可能會出現(xiàn)間隔不準的情況,導(dǎo)致動畫卡頓。
使用 requestAnimationFrame
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>setInterval vs requestAnimationFrame</title> <style> body { margin: 0; overflow: hidden; background-color: #f0f0f0; } .box { position: absolute; width: 50px; height: 50px; background-color: red; } </style> </head> <body> <div class="box"></div> <script> const box = document.querySelector('.box'); let posX = 0; // 使用 requestAnimationFrame 來創(chuàng)建動畫 function animate() { posX += 2; if (posX > window.innerWidth) { posX = -50; } box.style.transform = `translateX(${posX}px)`; requestAnimationFrame(animate); // 每次動畫完成后請求下一幀 } requestAnimationFrame(animate); // 啟動動畫 </script> </body> </html>
示例中,回調(diào)函數(shù)被安排在瀏覽器下次重繪前執(zhí)行。由于它與瀏覽器的渲染周期同步,它能確保每幀動畫的執(zhí)行間隔更精確,而且通常與瀏覽器的刷新率(一般是每秒 60 幀)一致。
更重要的是,當頁面切換到后臺時,requestAnimationFrame 會自動暫停,減少資源消耗。而 setInterval 則不受影響,可能會繼續(xù)執(zhí)行,浪費資源。
2.2 使用 Web Workers
如果需要一個不依賴瀏覽器渲染循環(huán)的精確定時器(例如處理后臺計算任務(wù)),可以使用 Web Workers。Web Workers 允許在后臺線程中運行 JavaScript 代碼,而不會阻塞主線程。通過 Web Workers 可以獲得更高精度的定時器控制,尤其適用于那些不涉及 UI 渲染的任務(wù)。
原理:
- Web Workers 不受瀏覽器的渲染影響,因此它們不會因為頁面切換到后臺而降低執(zhí)行頻率。
- 由于其與 UI 線程分離,Web Workers 的定時器可以維持更穩(wěn)定的間隔,避免了主線程中的限制。
舉個 ??
場景:我們需要一個精確的定時器來執(zhí)行計算任務(wù),例如:每隔 50ms 進行一次計算。這個任務(wù)是一個后臺計算任務(wù),與 UI 渲染無關(guān),而且希望它在頁面切換到后臺時不受影響。
1、創(chuàng)建 Web Worker 來處理后臺任務(wù)。
// worker.js // 接收到主線程的消息后執(zhí)行定時任務(wù) let intervalId; self.onmessage = function (e) { if (e.data === 'start') { // 啟動定時器,每50毫秒執(zhí)行一次計算 intervalId = setInterval(() => { const result = Math.random(); // 模擬計算任務(wù)(例如生成隨機數(shù)) self.postMessage(result); // 將計算結(jié)果發(fā)送回主線程 }, 50); } else if (e.data === 'stop') { // 停止定時器 clearInterval(intervalId); } };
2、主線程代碼(index.html)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Web Worker</title> <style> body { margin: 0 auto; display: flex; justify-content: center; align-items: center; } .container { text-align: center; } #result { font-size: 24px; margin-top: 20px; } </style> </head> <body> <div class="container"> <h3>Web Worker 精確定時器</h3> <button id="startBtn">Start</button> <button id="stopBtn">Stop</button> <div id="result"></div> </div> <script> // 創(chuàng)建一個新的 Web Worker const worker = new Worker('worker.js'); const startBtn = document.getElementById('startBtn'); const stopBtn = document.getElementById('stopBtn'); // 顯示計算結(jié)果的元素 const resultDiv = document.getElementById('result'); // 監(jiān)聽 Web Worker 返回的數(shù)據(jù) worker.onmessage = function (e) { const result = e.data; resultDiv.innerText = `計算結(jié)果: ${result}`; }; // 啟動 Web Worker 中的定時任務(wù) startBtn.addEventListener('click', function () { worker.postMessage('start'); }); // 停止 Web Worker 中的定時任務(wù) stopBtn.addEventListener('click', function () { worker.postMessage('stop'); }); </script> </body> </html>
特點:精確性、獨立性、穩(wěn)定性。
實際應(yīng)用:
這種精確的定時器非常適用于一些不涉及 UI 更新的任務(wù),如:
- 后臺數(shù)據(jù)計算任務(wù)(例如處理大量數(shù)據(jù)、定時抓取數(shù)據(jù)等)
- 數(shù)據(jù)同步(例如定時發(fā)送請求到服務(wù)器)
- 游戲引擎的后臺計算任務(wù)
- AI 算法或大數(shù)據(jù)分析等后臺任務(wù)
使用 Web Workers 可以確保這些任務(wù)的執(zhí)行頻率更加穩(wěn)定且不受主線程的干擾,非常適用于后臺計算任務(wù),不會因為頁面切換到后臺而降低性能。
2.3 使用 performance.now()
performance.now() 提供了比 Date.now() 更高精度的時間戳(微秒級別),適用于需要高精度計時的場景,比如高頻率任務(wù)或精細調(diào)度。與 setTimeout 或 setInterval 結(jié)合使用時,能更精確地控制時間間隔。
原理:
- performance.now() 提供的時間戳是相對于頁面加載開始的,而不是當前時間,這使得它能夠在不同設(shè)備和系統(tǒng)中提供一致的精度。
- 因為 performance.now() 的精度更高,可以計算出每次調(diào)用的精確間隔,避免了定時器本身的偏差。
舉個 ??
<div class="container"> <h3>高精度定時器</h3> <div id="result">計時中...</div> </div> <script> let lastTimestamp = performance.now(); // 初始化時間戳 let totalTime = 0; // 累積經(jīng)過的時間 let count = 0; // 計數(shù)器,記錄執(zhí)行次數(shù) function tick() { const now = performance.now(); const elapsed = now - lastTimestamp; // 計算兩次回調(diào)之間的時間間隔 lastTimestamp = now; totalTime += elapsed; count++; // 更新顯示的時間間隔 document.getElementById('result').innerHTML = ` 總執(zhí)行時間: ${totalTime.toFixed(2)} 毫秒<br> 執(zhí)行次數(shù): ${count} 次<br> 平均間隔: ${(totalTime / count).toFixed(2)} 毫秒 `; setTimeout(tick, 50); // 動態(tài)調(diào)整下一次執(zhí)行的間隔 } // 啟動高精度定時器 tick(); </script>
注意一點:(totalTime / count) 和 elapsed 之間會出現(xiàn)偏差。原因如下:
1、時間累積誤差(Cumulative Error)
每次執(zhí)行回調(diào)時,elapsed 計算的是當前回調(diào)與上次回調(diào)之間的時間間隔,而 (totalTime / count) 是根據(jù)所有回調(diào)的累積時間來計算的平均間隔。
由于每次調(diào)用時 elapsed 只是當前執(zhí)行的時間差,而 (totalTime / count) 是逐步累加的,可能會隨著時間的推移引入微小的誤差,尤其是在高頻率任務(wù)中。
這類誤差累積可能使得兩個值之間的差異逐漸增大,尤其是在多次計算的情況下。
2、setTimeout 的不精確性
setTimeout 不能精確地按照指定的間隔時間執(zhí)行回調(diào),它會受到事件隊列的調(diào)度、CPU 負載、瀏覽器渲染周期等因素的影響。每次 setTimeout 的延遲可能會有所不同,導(dǎo)致實際的回調(diào)執(zhí)行時間存在波動。這就會導(dǎo)致 elapsed 的值和 (totalTime / count) 之間產(chǎn)生差異。
例如,在高負載的情況下,回調(diào)可能會比預(yù)期的 50ms 晚幾毫秒才執(zhí)行,從而導(dǎo)致 elapsed 比實際的 50ms 值略大,而 (totalTime / count) 則會反映出一個平均值,減小了這種偏差。
3、調(diào)度隊列的延遲
在瀏覽器的事件循環(huán)(Event Loop)中,任務(wù)隊列中的回調(diào)會按照先后順序執(zhí)行。即使設(shè)置了 50 ms 的延遲,但回調(diào)的執(zhí)行也可能因為其他任務(wù)(例如渲染、計算等)在任務(wù)隊列中排隊而延遲執(zhí)行。這意味著 elapsed 的計算不一定精確地反映實際的間隔時間,導(dǎo)致 (totalTime / count) 和 elapsed 出現(xiàn)偏差。
4、performance.now() 與 setTimeout 調(diào)度的差異
雖然 performance.now() 提供了高精度的時間戳,但它只能準確地提供當前的時間點,而 setTimeout 并不是在精確的時間點執(zhí)行回調(diào),而是放入任務(wù)隊列中,并等待瀏覽器調(diào)度。
因此,實際的時間間隔(通過 elapsed 計算的)可能會比理想的間隔稍長,尤其是在高負載或繁忙的瀏覽器中。
總結(jié),這些誤差是由瀏覽器的定時器實現(xiàn)機制所決定的,無法完全避免。但通過合理的設(shè)計和優(yōu)化,可以減小這種誤差對精度的影響。
2.4 避免重疊的 setTimeout 或 setInterval 調(diào)用
瀏覽器的定時器方法(如 setTimeout 或 setInterval)可能因為任務(wù)隊列的積壓、事件循環(huán)的阻塞、或頁面切換等原因,導(dǎo)致實際回調(diào)執(zhí)行的間隔不準確。
解決方法:
- 逐步遞歸調(diào)用:代替 setInterval 使用遞歸的 setTimeout,確保定時器按期望的間隔執(zhí)行,而不是固定的時間間隔。
- 調(diào)整回調(diào)中的邏輯:每次回調(diào)時都重新計算下一個定時器的執(zhí)行時間,而不是使用固定的時間間隔。
舉個 ??
let lastTime = performance.now(); let count = 0; function recursiveTimeout() { const now = performance.now(); const elapsed = now - lastTime; if (elapsed >= 100) { // 精確的 100ms 間隔 count++; console.log(`回調(diào) #${count}, 時間: ${now}`); lastTime = now; } // 遞歸調(diào)用 setTimeout,確保間隔準確 setTimeout(recursiveTimeout, 100 - (elapsed % 100)); } recursiveTimeout(); // 啟動遞歸定時器
2.5 在 Node.js 中使用 setImmediate() 或 setInterval()
在 Node.js 環(huán)境中,setImmediate() 和 setInterval() 可以提供比普通的 setTimeout() 更高精度的定時器。
setImmediate() 用于在當前事件循環(huán)結(jié)束后執(zhí)行回調(diào),通常比 setTimeout(fn, 0) 更精確。
3. 為什么需要精確的定時器?
精準的定時器對于某些應(yīng)用至關(guān)重要,如:
- 動畫渲染:動態(tài)渲染的 UI 需要高精度的定時器來確保動畫流暢。
- 游戲引擎:游戲引擎需要根據(jù)時間精確更新游戲邏輯。
- 實時通信:實時系統(tǒng)(如視頻通話、實時數(shù)據(jù)同步等)需要保證定時任務(wù)的準確性,以確保數(shù)據(jù)的實時性和一致性。
通過優(yōu)化定時器的精度,可以提升應(yīng)用的流暢度、實時性和響應(yīng)速度,確保用戶體驗不被延遲或不精確的定時器行為影響。
到此這篇關(guān)于setInterval 不準的原因及問題解決方案的文章就介紹到這了,更多相關(guān)setInterval 不準內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
javascript 操作cookies及正確使用cookies的屬性
在 JS(JavaScript) 操作cookies比較復(fù)雜,在 ASP 里面我們只需要知道 cookie 的名稱、cookie 的值就行了,而 JS 里面,我們面對的是 cookie 的字符串,你自己編寫這個字符串寫入客戶端,然后自己解析這個字符串。2009-10-10JS面向?qū)ο缶幊袒A(chǔ)篇(一) 對象和構(gòu)造函數(shù)實例詳解
這篇文章主要介紹了JS面向?qū)ο缶幊虒ο蠛蜆?gòu)造函數(shù),結(jié)合實例形式詳細分析了JS面向?qū)ο缶幊虒ο蠛蜆?gòu)造函數(shù)具體概念、原理、使用方法及操作注意事項,需要的朋友可以參考下2020-03-03基于JavaScript實現(xiàn)手機短信按鈕倒計時(超簡單)
在淘寶等購物網(wǎng)站,我們都會看到一個發(fā)送短信倒計時的按鈕,究竟是如何實現(xiàn)的呢?下面小編通過本篇文章給大家分享一段代碼關(guān)于js實現(xiàn)手機短信按鈕倒計時,需要的朋友參考下2015-12-12