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。

相关推荐
GIS之路18 小时前
GDAL 实现矢量裁剪
前端·python·信息可视化
是一个Bug18 小时前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213818 小时前
React面向组件编程
开发语言·前端·javascript
学历真的很重要18 小时前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
持续升级打怪中18 小时前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路18 小时前
GDAL 实现矢量合并
前端
hxjhnct18 小时前
React useContext的缺陷
前端·react.js·前端框架
冰暮流星19 小时前
javascript逻辑运算符
开发语言·javascript·ecmascript
前端 贾公子19 小时前
从入门到实践:前端 Monorepo 工程化实战(4)
前端
菩提小狗19 小时前
Sqlmap双击运行脚本,双击直接打开。
前端·笔记·安全·web安全