setInterval 是 JavaScript 中用于定时执行任务的常用方法。它的基本语法如下:
const intervalId = setInterval(callback, delay, ...args);
- callback 是要执行的函数。
- delay 是每次执行之间的时间间隔(以毫秒为单位)。
- args 是传递给 callback 的附加参数。
但是,在实际使用中,可能会发现 setInterval 并不总是精确地按照预期的间隔时间来执行任务。这是因为 JavaScript 中的定时器并不是绝对精准的。
1. 为什么定时器不精准?
1.1 单线程执行
JavaScript 是单线程的,这意味着所有的任务都是在一个线程上排队执行的。虽然通过 setInterval 设置了定时器,想要周期性地执行某个任务,但如果在回调执行时主线程正在处理其他任务(比如执行计算、渲染 UI、处理用户事件等),就会导致回调函数被延迟执行,从而使定时器间隔时间不准确。
举个 🌰:设置了一个定时器,每隔 100ms 执行一次回调。
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);
// 这里模拟一个会占用主线程的长任务
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 后首次触发,然后执行 setTimeout 长任务。
t = 500ms:长任务结束,主线程被释放,但主线程的阻塞导致 setInterval 的回调没有按预定时间执行。
t = 100ms:setInterval 第一次回调,但由于主线程被阻塞,回调没有执行。
t = 600ms:setInterval 执行下一次回调。后面的 callback 也对应推迟。
输出日志:
主线程被占用时,定时器回调会被推迟,造成时间间隔的不准确。这种情况发生在主线程繁忙时,任务执行排队,定时器回调会“排队”到后面,等待后续执行。
1.2 任务排队和事件循环
JavaScript 使用事件循环来调度任务。setInterval 的回调是被放入 宏任务队列 中的,而宏任务队列的执行是有优先级的,只有在当前执行栈为空时,才会去执行队列中的任务。如果在执行回调时有其他更优先的任务(如 UI 渲染、用户输入处理等),那么定时器回调可能会被延迟。
举个 🌰
let count = 0;
const startTime = Date.now();
// 设置一个定时器,每 100ms 执行一次回调
const intervalId = setInterval(() => {
count++;
console.log(`setInterval callback executed: ${count}`);
}, 100);
// 模拟高优先级任务
console.log('Start of the script execution,time:', Date.now() - startTime);
// 使用 Promise 模拟一个高优先级任务
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);
});
// 模拟低优先级的异步任务
setTimeout(() => {
console.log('setTimeout task executed,time:', Date.now() - startTime);
}, 50);
console.log('End of the script execution,time:', Date.now() - startTime);
微队列优先执行,导致 setInterval 和 setTimeout 的回调被推迟。
1.3 回调执行时间的累积误差
setInterval 设定的间隔时间只是开始和结束之间的期望时间,但回调函数的