不那么准时的setTimeout和setInterval
setTimeout和setInterval的执行时间误差
在我们常见观念中,setTimeout 和 setInterval 都是用来在指定时间定时执行代码的函数。 setTimeout用于在指定的时间后执行一次代码,而setInterval则用于在指定的时间间隔内重复执行代码。
如题,下面的代码,很多人会下意识的认为 setTimeout 会在1秒后输出,setInterval 每1秒输出一次
但是实际情况中,setTimeout和setInterval的执行时间是有一定误差的。
javascript
const start = performance.now(); // 记录开始时间
setTimeout(() => {
const end = performance.now();
console.log(`setTimeout实际执行时间:${end - start}ms`);
}, 1000);
javascript
const start = performance.now(); // 记录开始时间
setInterval(() => {
const end = performance.now();
console.log(`setInterval实际执行时间:${end - start}ms`);
}, 1000);
实际上执行时间会大于等于指定的延迟时间

这是为什么呢?定时器始终存在实际最小执行间隔,即使指定的延迟时间为0ms,也会存在一定的延迟。
1、JS 引擎机制
这是因为JavaScript是单线程执行的,而 setTimeout 和 setInterval 都是异步函数, 当JavaScript引擎执行到 setTimeout 或 setInterval 时,会将其放入事件队列中,定时器的回调函数必须等待主线程空闲时才能执行。而主线程空闲的时间是不确定的,取决于当前执行的代码的耗时。 即使主线程空闲,浏览器也需要一定时间调度回调函数(属于引擎内部的 "调度开销"),这个开销通常在 1ms~4ms 之间
2、HTML5 规范
-
HTML5 规范明确规定了定时器的最小延迟限制为4ms,嵌套层级较浅时最小延迟可能低至1ms。
-
对于嵌套层级 ≥ 5 的定时器(即定时器回调中再次创建定时器,形成嵌套),浏览器会强制将最小延迟限制为 4ms。
-
处于后台标签页中的计时器也可能会被限制为1秒的最小延迟
因此,对于实际应用中,我们不能依赖 setTimeout 和 setInterval 的精确执行时间,而应该根据实际需求来调整定时器的延迟时间。
因此,对于实际应用中,我们不能依赖 0ms 延迟实现高精度逻辑(比如动画帧)
推荐使用 requestAnimationFrame(专门为动画设计,与浏览器刷新频率同步)
解决方法
1、调整延迟时间 根据实际需求,合理调整定时器的延迟时间。如果对执行时间有严格要求,建议使用 Date.now() 或performance.now() 来计算实际执行时间,而不是依赖定时器的回调函数。
2、使用 requestAnimationFrame requestAnimationFrame 是浏览器提供的用于动画循环的函数,它会在浏览器下一次重绘之前执行回调函数。与浏览器刷新频率同步,使用 requestAnimationFrame 可以实现更精确的动画效果。
setInterval存在的问题
除了最小延迟时间外,setInterval 还存在一些额外问题,主要表现在以下几个方面:
1、任务堆叠(执行耗时 > 设定间隔)
如果回调中的执行耗时超过间隔时间,setInterval 可不会等你,而是会立即执行下一个回调,它会无视上次回调是否执行完毕,将新的回调推入队列,导致任务在队列中堆积,主线程空闲后,这些回调会连续地执行,失去间隔意义
javascript
const startTime = Date.now();
let count = 0;
setInterval(() => {
count++;
// 每秒先输出当前执行信息
console.log(`执行第${count}次,累计运行${((Date.now() - startTime) / 1000).toFixed(1)}s`);
// 模拟耗时操作(阻塞2s)
const start = Date.now();
while (Date.now() - start < 2000) {} // 阻塞2000ms
// 输出任务完成信息,并标记时间点
if (count === 1) {
console.log("第一次任务执行完成(约3秒后)");
} else {
console.log("任务执行完成(间隔约2秒)");
}
}, 1000); // 间隔1000ms
// 实际为除了第一次是3s后执行完成,后续都是2s后执行完成,不会按照预设的1s间隔执行
如果回调中的执行耗时超过间隔时间,实际间隔时间就是执行任务消耗的时间,而不是设定的值
2、误差累积(时间不准)
上面说过,由于js的单线程机制,setInterval 无法保证精确间隔,实际输出时间间隔可能大于设定的时间,因为js是单线程的,游览器最小间隔限制:浏览器通常有 4ms 最小延迟(即使设 0ms),没有0延时定时器,包括setTimeout。
因此,在使用 setInterval 时,需要注意任务的执行耗时,避免耗时过长导致误差累积。
3、内存泄漏(忘记清除)
忘记清理 setTimeout 只会导致一个多余的回调执行。但忘记 clearInterval 会导致回调函数无限执行,造成严重的内存泄漏,且页面跳转也不会自动停止(在 SPA 中最为明显)
解决办法
手动写一个 setTimeout 递归封装的「安全版定时器」,解决 setInterval 可能出现的任务堆叠等问题,确保前一次回调完全执行完毕后,才触发下一次
简单示例:
js
function myInterval(fn, delay) {
// 递归执行函数
const run = () => {
fn(); // 执行任务
setTimeout(run, delay); // 任务完成后,才触发下一次
};
// 启动首次执行
setTimeout(run, delay);
}
使用效果
js
const startTime = Date.now();
let count = 0;
myInterval(() => {
count++;
const start = Date.now();
while (Date.now() - start < 1000) {} // 阻塞1000ms
console.log(`执行第${count}次,累计运行${((Date.now() - startTime) / 1000).toFixed(1)}s`);
}, 1000);
每2s间隔执行,这样确保了每次回调执行完毕后,才会触发下一次,避免了任务堆叠问题。
总结
在开发中,在大多数需要轮询定时器的场景下,使用递归的 setTimeout 相对更安全、更可控,使用 setInterval 时要做到心中有数,避免产生bug。