setTimeout为什么不精确

1. JS是单线程的

JavaScript 是单线程的,意味着代码是顺序执行的。当调用 setTimeout 时,它会将回调函数加入到事件队列中,等待当前线程空闲后执行。因此,如果当前线程有其他任务在执行(比如正在处理一个大的循环或复杂的操作),setTimeout 的回调会延迟执行,从而导致超时不准确。

基于上面的情况下,还有下面的另一种影响

JavaScript 的执行是基于事件循环(Event Loop)的。setTimeout 并不立即执行,而是将回调函数放入事件队列中,直到当前执行栈清空。所以即使设定了精确的延迟,回调的执行时间也可能因为事件循环的队列处理和任务调度的顺序而有所不同

也可以理解为设置setTimeout后的执行时间=执行栈清空的时间+duration

2. 最小延迟时间

在一些浏览器中,setTimeout 的最小延迟时间可能并不是你设置的值。根据不同的环境(特别是在浏览器中),延迟时间可能会被浏览器限制,通常会有一个最小值,通常是 4 毫秒(但在一些浏览器中可能更长)。比如,如果你设置了 setTimeout(func, 1),可能实际上会延迟 4 毫秒或更长。

当然也有另外的说法是在五级嵌套的情况下才会有这个四毫秒的延时。

3. 系统和硬件的影响

不同操作系统和硬件的性能差异也会影响 setTimeout 的精确度。如果系统负载较高,或者操作系统有时间调度策略,setTimeout 可能会稍微延迟执行。

4.失活页面

失活页面(例如,用户切换到其他标签页或者浏览器处于后台时)会影响 setTimeout 和其他定时器的精度

页面不再是活动页面时。浏览器会减少在后台标签页中执行 JavaScript 代码的频率,以节省资源和提升性能

当页面处于非活动状态时,浏览器会延迟执行计时器回调,甚至可能会完全忽略一些定时器,直到页面恢复活动。

如何解决

要解决  setTimeout 的不精确性,可以采取一些策略来提高定时任务的精确度。虽然无法完全消除 JavaScript 的单线程模型和事件循环带来的影响,但可以通过以下方法来优化:

1. 使用递归 setTimeout 模式

为了避免因为系统延迟或事件队列的影响,使得每次 setTimeout 的实际延迟时间都略微偏离预期,可以通过递归的方式动态调整下次的超时执行时间。也就是说,每次回调完成后,通过递归调用 setTimeout,并根据当前时间动态调整下次的超时:

function preciseTimeout(callback, interval) {
  const startTime = Date.now();  // 获取当前时间
  function loop() {
    const elapsedTime = Date.now() - startTime;  // 计算过去的时间
    const delay = Math.max(0, interval - (elapsedTime % interval));  // 计算下次的延迟时间

    setTimeout(function() {
      callback();
      loop();  // 递归调用,以保持精度
    }, delay);
  }
  
  loop();
}

// 示例:每 100 毫秒执行一次任务
preciseTimeout(() => {
  console.log('执行任务', Date.now());
}, 100);

2. 使用 requestAnimationFrame (可以用于高精度动画)

使用 requestAnimationFrame 来替代 setTimeoutrequestAnimationFrame 会在浏览器的每一帧渲染之前调用,这样可以确保任务的执行和屏幕的刷新同步。

function preciseAnimation(callback) {
  let lastTime = 0;
  function animate(timestamp) {
    const deltaTime = timestamp - lastTime;  // 获取当前时间和上次执行的时间差
    if (deltaTime >= 100) {  // 每 100 毫秒执行一次
      callback();
      lastTime = timestamp;
    }
    requestAnimationFrame(animate);  // 请求下一帧
  }
  
  requestAnimationFrame(animate);
}

// 示例:每 100 毫秒执行一次任务
preciseAnimation(() => {
  console.log('执行任务', Date.now());
});

requestAnimationFrame 更适合用于动画和精确的时间控制,因为它会自动适应屏幕刷新率,通常是 60 FPS,因此会比setTimeout更平滑且精确。

虽然RAF不会收到事件循环的影响,也不受失活页面的影响,但是如果系统运行电脑卡了,会影响到渲染帧,其他页面的卡死也会影响到渲染帧,使得不精准

3. 增加多次检查

如果任务执行的时机非常重要,可以通过多次检查和校正延迟来提高精度。通过设定一个非常短的时间间隔并多次调整,可以使误差减少。

例如,假设你每 10 毫秒检查一次时间,然后精确调整任务执行的时机:

function preciseCheck(callback, interval) {
  const targetTime = Date.now() + interval;  // 设定目标时间

  function check() {
    if (Date.now() >= targetTime) {
      callback();  // 执行任务
    } else {
      setTimeout(check, Math.max(0, targetTime - Date.now()));  // 调整等待时间
    }
  }

  check();
}

// 示例:精确地每 100 毫秒执行一次任务
preciseCheck(() => {
  console.log('执行任务', Date.now());
}, 100);

在这个方法中,我们通过每次检查是否已经达到目标时间,并调整下次检查的超时,来确保任务尽量按预期执行。适用于需要高精度控制时间间隔的任务,特别是对于不太依赖 CPU 密集型操作的任务

4. 使用Web Workers

在主线程忙碌的时候,多开一个线程来执行计时,不会受到渲染帧和主线程的影响,是比较适合的方案

// 在主线程中创建一个 Worker
const worker = new Worker('worker.js');

// 在 Worker 中执行复杂任务
worker.postMessage('start');

worker.onmessage = function(e) {
  console.log('Worker 回传消息:', e.data);
};

// worker.js
onmessage = function() {
  // 执行复杂的计算或任务
  setInterval(() => {
    postMessage('任务完成');
  }, 100);  // 使用 setInterval,Web Worker 中执行任务不会阻塞主线程
};

总结:

虽然 JavaScript 的 setTimeout 本身无法做到非常精确的定时任务,但通过以下几种方法可以提高精度:

  • 使用递归 setTimeout 来动态调整超时延迟。
  • 如果任务涉及动画,使用 requestAnimationFrame
  • 使用多次检查和调整延迟来减少误差。
  • 在复杂计算任务中使用 Web Workers 来避免主线程阻塞(推荐,既不受主线程的影响也不受渲染帧的影响)。

给大家分享一些JavaScript的常见面试题 需要的戳这里~

  • 面试官:不会冒泡的事件有哪些?
  • 面试官:mouseEnter 和 mouseOver 有什么区别?
  • 面试官:MessageChannel 是什么,有什么使用场景?
  • 面试官:async、await 实现原理
  • 面试官:Proxy 能够监听到对象中的对象的引用吗?
  • 面试官:如何让 var [a, b]= {a: 1,b:2}解构赋值成功?
  • 面试官:下面代码会输出什么?
  • 面试官:描述下列代码的执行结果
  • 面试官:什么是作用域链?
  • 面试官:bind、call、apply 有什么区别?如何实现-个bind?
  • 面试官:common.js和es6中模块引入的区别?
  • 面试官:说说 vue3 中的响应式设计原理
  • 面试官:script标签放在header里和放在body底部里有什么区别?
  • 面试官:下面代码中,点击“+3”按钮后,age 的值是什么?
  • 面试官:Vue中,created和mounted两个钩子之间调用时间差值受什么影响?
  • 面试官:vue中,推荐在哪个生命周期发起请求?面试官:
  • 面试官:不会冒泡的事件有哪些?
  • 面试官:mouseEnter 和 mouseOver 有什么区别?
  • 面试官:为什么Node在使用es module时必须加上文件扩展名?
  • 面试官:React Portals 有什么用?
  • 面试官:react 和 react-dom 是什么关系?
  • 面试官:MessageChannel 是什么,有什么使用场景?
  • 面试官:React 中为什么不直接使用 requestIdleCallback?
  • 面试官:为什么 react 需要 fiber 架构,而 Vue 却不需要?

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐