跳到主要内容

认识

2023年03月19日
柏拉文
越努力,越幸运

一、认识


setTimeout() 方法设置一个定时器,该定时器在定时器到期后执行一个函数或指定的一段代码。

二、语法


const timeoutId = setTimeout(()=>{},delay,arg1,arg2,……,argN);
  • function: function 是你想要在到期时间 (delay毫秒) 之后执行的函数。

  • delay: 延迟的毫秒数 (一秒等于 1000 毫秒),函数的调用会在该延迟之后发生。如果省略该参数,delay 取默认值 0,意味着马上执行,或者尽快执行。

  • arg1,arg2,……,argN: 附加参数,一旦定时器到期,它们会作为参数传递给function,传参语法如下所示:

  • timeoutId: 返回值timeoutID是一个正整数,表示定时器的编号。这个值可以传递给clearTimeout()来取消该定时器。需要注意的是 setTimeout()setInterval() 共用一个编号池,技术上,clearTimeout()clearInterval() 可以互换。但是,为了避免混淆,不要混用取消定时函数。

三、特点


3.1 超时延迟

除了最小延时之外,定时器仍然有可能因为当前页面(或者操作系统/浏览器本身)被其他任务占用导致延时。 需要被强调是, 直到调用 setTimeout() 的主线程执行完其他任务之后,回调函数和代码段才能被执行。出现这个结果的原因是,尽管setTimeout0ms 的延迟来调用函数,但这个任务已经被放入了队列中并且等待下一次执行;并不是立即执行;队列中的等待函数被调用之前,当前代码必须全部运行完毕,因此这里运行结果并非预想的那样。

console.time('timer1');
setTimeout(() => {
console.timeEnd('timer1');
console.log('原生的 setTimeout');
});

timer1 的结果始终 >= 4 ms

3.2 最小延时 >=4ms

在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms

console.time('timer1');
setTimeout(() => {
console.timeEnd('timer1');
console.log('原生的 setTimeout');
});

timer1 的结果始终 >= 4 ms, 出现原因有两种:

  • 原因一: 已经执行的 setInterval 的回调函数阻塞

  • 原因二: 因为 setTimeout 是一个宏任务,它的指定时间指的是:进入主线程的时间。所以什么时候可以执行 callback,需要看主线程前面还有多少任务待执行。

  • 原因二: 在嵌套函数调用 setTimeout() 时(嵌套级别达到特定深度时): ChromeFirefox 中, 定时器的第 5 次调用被阻塞了;在 Safari 是在第 6 次;Edge 是在第 3

四、对比


4.1 setTimeoutsetInterval 对比

  • 从功能方面:

    • setTimeout间隔一段时间之后执行一次调用

    • setInterval每间隔一段时间循环调用,直至clearInterval结束

  • 从内存方面:

    • setTimeout 间隔的时间到达后,将回调事件放入到事件触发线程的队列中,且这个操作只有一次

    • setInterval 间隔的时间到达后,也同样将回调事件放入到事件触发线程的队列中。不过下次的间隔时间到达后,回调会检测事件队列中是否已经有回调函数

      • 如果已经有回调函数: 那么该次的回调函数不会添加到事件队列中,该次的调用被废弃,这就是setInterval 调用被废弃的原因
      • 如果不存在回调函数: 回调函数再次放入事件队列,等待被执行。如果如果回调函数中处理的时长 > 间隔时长 , 那么上一个回调函数才从事件队列出去,下一个回调函数因为间隔时间到了加入事件队列。这样, 一个回调函数执行完毕,就开始执行另一个回调,造成 setInterval 没有间隔的重复执行 ---- setTimeout 模拟 setInterval 的原因: 很多情况下,我们并不能清晰的把控处理程序所消耗的时长,为了我们能按照一定的间隔周期性的触发定时器

4.2 requestAnimationFrame动画与setInterval动画、setTimeout动画对比

  • setTimeout 动画、setInterval 动画 : 会造成掉帧、卡顿、闪屏 的问题

    • 原因一: setTimeout的执行时间并不是确定的。在JS中,setTimeout任务被放进事件队列中,只有主线程执行完才会去检查事件队列中的任务是否需要执行,因此setTimeout的实际执行时间可能会比其设定的时间晚一些。

    • 原因二: 刷新频率受屏幕分辨率和屏幕尺寸的影响,因此不同设备的刷新频率可能会不同,而setTimeout只能设置一个固定时间间隔,这个时间不一定和屏幕的刷新时间相同。

    以上两种情况都会导致setTimeout的执行步调和屏幕的刷新步调不一致。在setTimeout中对dom进行操作,必须要等到屏幕下次绘制时才能更新到屏幕上,如果两者步调不一致,就可能导致中间某一帧的操作被跨越过去,而直接更新下一帧的元素,从而导致丢帧现象。

  • requestAnimationFrame动画: 与setTimeoutsetInterval相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。换句话说就是,requestAnimationFrame 的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象。

四、问题


4.1 对 setTimeout 的理解

  • 缺点: 它通过设定间隔时间来不断改变图像位置,达到动画效果。但是容易出现卡顿、抖动的现象;原因是:

    • setTimeout任务被放入异步队列,只有当主线程任务执行完后才会执行队列中的任务,因此实际执行时间总是比设定时间要晚;

    • setTimeout的固定时间间隔不一定与屏幕刷新间隔时间相同,会引起丢帧

4.2 定时器为什么是不可靠的?

说明: setTimeout/setInterval无法保证准时执行回调函数,定时器的时间间隔设为0,也会有几毫秒的延迟,定时器为什么是不可靠的

  1. 超时延迟: JavaScript 中有一个主线程和一个调用栈,所有的任务都会被放到调用栈等待主线程执行。JavaScript 执行时,同步任务都在主线程上执行,前进如果产生微任务,在同步任务执行完成后清空微任务,这时候是一个完整的宏任务+微任务 完成,此后还可能会有更新渲染。之后JavaScript询问事件触发线程,是否有待执行的回调函数,如果有就会加入到执行栈中交给主线程执行。所以,如果同步任务、或者同步任务完成之后有可能触发的页面更新渲染这些耗时都有可能使得定时器回调触发延迟

  2. 最小延迟 >= 4ms: 在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度),或者是由于已经执行的 setInterval 的回调函数阻塞导致的。

  3. 未被激活的 tabs 的定时最小延迟>=1000ms

4.3 未被激活的 tabs 的定时最小延迟 >=1000ms

为了优化后台 tab 的加载损耗(以及降低耗电量),在未被激活的 tab 中定时器的最小延时限制为 1S(1000ms)Firefoxversion 5 (see bug 633421开始采取这种机制,1000ms 的间隔值可以通过 dom.min_background_timeout_value 改变。Chromeversion 11 (crbug.com/66078) 开始采用。

4.4 谈谈你对 timeoutID 的认识以及取消函数的认识?

setTimeout 返回的timeoutIDsetInterval 返回的 timeoutID 共用一个编号池,技术上,clearTimeoutclearInterval 可以互换、混用。但是为了避免混淆,尽量不要混用取消定时函数。

function debounce(fn, wait) {
let timer = null;
return function (...args) {
if (timer) {
clearInterval(timer);
timer = null;
}
timer = setTimeout(() => {
fn.apply(this, args);
}, wait);
};
}