关于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的详解建议参考这篇文章

总结

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

相关推荐
小阮的学习笔记10 分钟前
Vue3中使用LogicFlow实现简单流程图
javascript·vue.js·流程图
YBN娜11 分钟前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=11 分钟前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
minDuck16 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
小政爱学习!36 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。41 分钟前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼1 小时前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k09331 小时前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
EricWang13581 小时前
[OS] 项目三-2-proc.c: exit(int status)
服务器·c语言·前端
September_ning1 小时前
React.lazy() 懒加载
前端·react.js·前端框架