关于setTimeout不精确问题详解及原理说明

setTimeout的定义和性质

引用MDN解释:

全局的 setTimeout() 方法设置一个定时器,一旦定时器到期,就会执行一个函数或指定的代码片段。

但其实我阅读在阮一峰老师的博客中更加受益匪浅,他是这么解释道:

setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。

并且他还提到

需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。

要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

结合这两段话可以理解为,在最后一个主执行栈的任务完毕后,这时候我们可以得到一个"最早的可空闲时间",这个时机我们就会用来执行setTimeout。

如图:

如果前面存在1~n个任务执行的耗时很长,那么就会影响到后续setTimeout的执行时机。也就造成了setTimeout精度丢失的问题。

setTimeout丢失精度的场景

我们来写一个测试案例,方便理解这个问题的抽象场景

js 复制代码
function delayTask() {
  var start = new Date().getTime();
  while (new Date().getTime() - start < 2000) {
    // 模拟耗时任务,延迟2秒
  }
  console.log("耗时任务完成");
}

console.log("开始执行");

setTimeout(function() {
  console.log("回调函数执行");
}, 1000);

delayTask();

console.log("执行结束");

在这个案例中,我们定义了一个 delayTask 函数,模拟一个耗时任务,需要延迟2秒才能完成。然后,在调用 setTimeout 设置延迟1秒后执行的回调函数之前,我们先调用了 delayTask 函数。

但是,运行上述代码,你会发现控制台的输出顺序如下:

text 复制代码
开始执行
耗时任务完成
回调函数执行
执行结束

可以看到,尽管我们设置了1秒的延迟,但是由于耗时任务的存在,实际执行回调函数的时间被推迟到了耗时任务完成之后。这就是 setTimeout 在遇到耗时任务时的延迟情况。

原理解释

出现这种情况的原理,我们可以结合它的执行时机,初步判断和js事件循环有关系。

我们先来回顾一下js事件循环

因为我们的js是一门单线程的语言,那么意味着在同一段时间内任务只能一个接着一个执行。

下一个任务必须等待上一个任务执行结束之后才能执行。

如果前一个任务耗时很长,后一个任务就不得不一直等着。

所以后来出了一种异步方案,能巧妙解决这个问题,通过将等待中的异步任务先挂起,先运行排在后面的任务,等到回调返回了结果,再回过头,把挂起的任务继续执行下去。

事件循环的过程可以简化为以下几个步骤:

  1. 执行同步任务:从执行栈中取出位于栈顶的同步任务,执行它。
  2. 处理微任务:检查微任务队列,依次执行队列中的微任务。微任务包括 Promisethen 方法、MutationObserver 的回调函数等。
  3. 更新渲染:如果需要更新页面的渲染,进行渲染操作。
  4. 处理宏任务:检查宏任务队列,选择其中最早进入队列的任务,执行它。宏任务包括定时器回调函数、网络请求响应等。
  5. 重复步骤 2-4,直到没有任务可以执行。

在这个事件循环的过程中,setTimeout 的回调函数实际上是一个宏任务。当调用 setTimeout 时,会将回调函数插入到宏任务队列中,然后继续执行下面的代码。

当所有的同步任务执行完毕后,事件循环会检查当前是否存在微任务。如果存在,会依次执行微任务队列中的任务。然后,事件循环会检查宏任务队列中是否有任务,选择最早进入队列的任务执行。

在这个过程中,如果遇到了耗时的任务或者其他异步操作,会阻塞当前的事件循环。也就是说,当一个宏任务执行的时间过长,可能会延迟后续的微任务执行以及其他宏任务的执行。这就是为什么在上述案例中,耗时任务导致 setTimeout 的回调函数被延迟执行的原因。

这个机制能够帮助我们对于出现这样问题的理解,由于执行栈中前面的任务未执行完,在setTimeout设定的delay后还没轮到它执行,所以会造成延时不准的问题。

解决方案

可以用requestAnimationFrame代替setTimeout的方案。

具体关于requestAnimationFrame和setTimeout的区别以及requestAnimationFrame的详解建议参考这篇文章

总结

最后希望这篇文章能够帮助到你~也作为自己平时学习的一个积累。如果有不足或不准确的地方,欢迎大家在评论区指出和讨论~感谢🙏

相关推荐
正小安1 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
小飞猪Jay3 小时前
C++面试速通宝典——13
jvm·c++·面试
_.Switch3 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光3 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   3 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   3 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web3 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常3 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇4 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr4 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui