setTimeout 和 setInterval:看似简单,但你不知道的使用误区

不那么准时的setTimeout和setInterval

setTimeout和setInterval的执行时间误差

在我们常见观念中,setTimeoutsetInterval 都是用来在指定时间定时执行代码的函数。 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 规范

  1. HTML5 规范明确规定了定时器的最小延迟限制为4ms,嵌套层级较浅时最小延迟可能低至1ms。

  2. 对于嵌套层级 ≥ 5 的定时器(即定时器回调中再次创建定时器,形成嵌套),浏览器会强制将最小延迟限制为 4ms。

  3. 处于后台标签页中的计时器也可能会被限制为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。

相关推荐
踩着两条虫2 分钟前
VTJ:页面管理功能
前端·低代码·ai编程
梦想的颜色3 分钟前
js document 节点增删改查、样式设计全解析
java·前端·javascript
nvvas15 分钟前
Could not resolve “@intlify/vue-devtools‘ node modules/. pnpm/vue-118n@9. 14
前端·javascript·vue.js
人道领域23 分钟前
【LeetCode刷题日记】225.用队列实现栈--三招实现栈操作(多种思维)
java·开发语言·算法·leetcode·面试
yqcoder23 分钟前
[特殊字符] Vue 3 组件通信全指南:从基础到进阶
前端·javascript·vue.js
梦想的颜色26 分钟前
js 去掉除法后得出的小数点
javascript·vue.js
爱上好庆祝27 分钟前
学习js第一天(出发新世界)
开发语言·前端·javascript·css·学习·html·ecmascript
木斯佳29 分钟前
前端八股文面经大全:秦丝科技前端(2026-04-24)·笔试深度解析
前端·笔试
喜欢吃鱿鱼30 分钟前
VUE项目 弹窗改为页面供其他项目嵌入iframe - 截取地址栏URL中的参数
前端·javascript·vue.js