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是一门单线程的语言,那么意味着在同一段时间内任务只能一个接着一个执行。
下一个任务必须等待上一个任务执行结束之后才能执行。
如果前一个任务耗时很长,后一个任务就不得不一直等着。
所以后来出了一种异步方案,能巧妙解决这个问题,通过将等待中的异步任务先挂起,先运行排在后面的任务,等到回调返回了结果,再回过头,把挂起的任务继续执行下去。
事件循环的过程可以简化为以下几个步骤:
- 执行同步任务:从执行栈中取出位于栈顶的同步任务,执行它。
- 处理微任务:检查微任务队列,依次执行队列中的微任务。微任务包括
Promise
的then
方法、MutationObserver
的回调函数等。 - 更新渲染:如果需要更新页面的渲染,进行渲染操作。
- 处理宏任务:检查宏任务队列,选择其中最早进入队列的任务,执行它。宏任务包括定时器回调函数、网络请求响应等。
- 重复步骤 2-4,直到没有任务可以执行。
在这个事件循环的过程中,setTimeout
的回调函数实际上是一个宏任务。当调用 setTimeout
时,会将回调函数插入到宏任务队列中,然后继续执行下面的代码。
当所有的同步任务执行完毕后,事件循环会检查当前是否存在微任务。如果存在,会依次执行微任务队列中的任务。然后,事件循环会检查宏任务队列中是否有任务,选择最早进入队列的任务执行。
在这个过程中,如果遇到了耗时的任务或者其他异步操作,会阻塞当前的事件循环。也就是说,当一个宏任务执行的时间过长,可能会延迟后续的微任务执行以及其他宏任务的执行。这就是为什么在上述案例中,耗时任务导致 setTimeout
的回调函数被延迟执行的原因。
这个机制能够帮助我们对于出现这样问题的理解,由于执行栈中前面的任务未执行完,在setTimeout设定的delay后还没轮到它执行,所以会造成延时不准的问题。
解决方案
可以用requestAnimationFrame代替setTimeout的方案。
具体关于requestAnimationFrame和setTimeout的区别以及requestAnimationFrame的详解建议参考这篇文章~
总结
最后希望这篇文章能够帮助到你~也作为自己平时学习的一个积累。如果有不足或不准确的地方,欢迎大家在评论区指出和讨论~感谢🙏