前言

在工作中利用定时器的场景十分多,但你会发现有时候定时器如同并没有依照咱们的预期去执行,比方咱们常遇到的setTimeout(()=>{},0)它有时候并不是按咱们预期的立马就执行。想要晓得为什么会这样,咱们首先须要理解Javascript计时器的工作原理。

定时器工作原理

为了了解计时器的外部工作原理,咱们首先须要理解一个十分重要的概念:计时器设定的延时是没有保障的。因为所有在浏览器中执行的JavaScript单线程异步事件(比方鼠标点击事件和计时器)都只有在它有空的时候才执行。

这么说可能不是很清晰,咱们来看上面这张图


图中有很多信息须要消化,然而齐全了解它会让您更好地理解异步JavaScript执行是如何工作的。这张图是一维的:垂直方向是(挂钟)工夫,单位是毫秒。蓝色框示意正在执行的JavaScript局部。例如,第一个JavaScript块执行大概18ms,鼠标点击块执行大概11ms,以此类推。

因为JavaScript一次只能执行一段代码(因为它的单线程个性),所以每一段代码都会“阻塞”其余异步事件的过程。这意味着,当异步事件产生时(如鼠标单击、计时器触发或XMLHttpRequest实现),它将排队期待稍后执行。

首先,在JavaScript的第一个块中,启动了两个计时器:一个10ms的setTimeout和一个10ms的setInterval。因为计时器是在哪里和什么时候启动的,它实际上在咱们理论实现第一个代码块之前触发,然而请留神,它不会立刻执行(因为线程的起因,它无奈这样做)。相同,被提早的函数被排队,以便在下一个可用的时刻执行。

此外,在第一个JavaScript块中,咱们看到鼠标单击产生。与此异步事件相关联的JavaScript回调(咱们永远不晓得用户何时会执行某个动作,因而它被认为是异步的)无奈立刻执行,因而,就像初始计时器一样,它被排队期待稍后执行。

在JavaScript的初始块实现执行后,浏览器会立刻问一个问题:期待执行的是什么?在本例中,鼠标单击处理程序和计时器回调都在期待。而后浏览器抉择一个(鼠标点击回调)并立刻执行它。计时器将期待到下一个可能的工夫,以便执行。

setInterval调用被废除

在click事件执行时,第20毫秒处,第二个setInterval也到期了,因为此时曾经click事件占用了线程,所以setInterval还是不能被执行,并且因为此时队列中曾经有一个setInterval正在排队期待执行,所以这一次的setInterval的调用将被废除

浏览器不会对同一个setInterval处理程序屡次增加到待执行队列。

实际上,咱们能够看到,当第三个interval回调被触发时,interval自身正在执行。这向咱们展现了一个重要的事实:interval并不关怀以后执行的是什么,它们将不加区别地排队,即便这意味着回调之间的工夫距离将被就义。

setTimeout/setInterval无奈保障准时执行回调函数

最初,在第二个interval回调执行实现后,咱们能够看到JavaScript引擎没有任何货色能够执行了。这意味着浏览器当初期待一个新的异步事件产生。当interval再次触发时,咱们会在50ms处失去这个值。然而这一次,没有任何货色妨碍它的执行,因而它立刻触发。

OK,总的来说造成JS定时器不牢靠的起因就是JavaScript是单线程的,一次只能执行一个工作,而setTimeout() 的第二个参数(延时工夫)只是通知 JavaScript 再过多长时间把当前任务增加到队列中。如果队列是空的,那么增加的代码会立刻执行;如果队列不是空的,那么它就要等后面的代码执行完了当前再执行定时器工作必须等主线程工作执行才可能开始执行,无论它是否达到咱们设置的工夫

这里咱们能够再来理解下Javascript的事件循环

事件循环

JavaScript中所有的工作分为同步工作与异步工作,同步工作,顾名思义就是立刻执行的工作,它个别是间接进入到主线程中执行。而咱们的异步工作则是进入工作队列期待主线程中的工作执行完再执行。

工作队列是一个事件的队列,示意相干的异步工作能够进入执行栈了。主线程读取工作队列就是读取外面有哪些事件。

队列是一种先进先出的数据结构。

下面咱们说到异步工作又能够分为宏工作与微工作,所以工作队列也能够分为宏工作队列微工作队列

  • Macrotask Queue:进行比拟大型的工作,常见的有setTimeout,setInterval,用户交互操作,UI渲染等;
  • Microtask Queue:进行较小的工作,常见的有Promise,Process.nextTick;
  1. 同步工作间接放入到主线程执行,异步工作(点击事件,定时器,ajax等)挂在后盾执行,期待I/O事件实现或行为事件被触发。
  2. 零碎后盾执行异步工作,如果某个异步工作事件(或者行为事件被触发),则将该工作增加到工作队列,并且每个工作会对应一个回调函数进行解决。
  3. 这里异步工作分为宏工作与微工作,宏工作进入到宏工作队列,微工作进入到微工作队列。
  4. 执行工作队列中的工作具体是在执行栈中实现的,当主线程中的工作全副执行结束后,去读取微工作队列,如果有微工作就会全副执行,而后再去读取宏工作队列
  5. 上述过程会一直的反复进行,也就是咱们常说的事件循环(Event-Loop)


这里更具体的内容能够看我之前的文章摸索JavaScript执行机制

导致定时器不牢靠的起因

当前任务执行工夫过久

JS 引擎会先执行同步的代码之后才会执行异步的代码,如果同步的代码执行工夫过久,是会导致异步代码提早执行的。

setTimeout(() => {  console.log(1);}, 20);for (let i = 0; i < 90000000; i++) { } setTimeout(() => {  console.log(2);}, 0);

这个按预期应该是会先打印出2,而后再打印1,但事实并不是如此,就算第二个定时器的工夫更短,但两头那个for循环的执行工夫远远超过了这两个定时器设定的工夫。

setTimeout 设置的回调工作是 依照程序增加到提早队列外面的,当执行完一个工作之后,ProcessDelayTask 函数会依据发动工夫和延迟时间来计算出到期的工作,而后 顺次执行 这些到期的工作。

在执行完后面的工作之后,下面例子的两个 setTimeout 都到期了,那么依照程序执行就是打印 12。所以在这个场景下,setTimeout 就显得不那么牢靠了。

提早执行工夫有最大值

包含 IE, Chrome, Safari, Firefox 在内的浏览器其外部以32位带符号整数存储延时。这就会导致如果一个延时(delay)大于 2147483647 毫秒 (大概24.8 天)时就会溢出,导致定时器将会被立刻执行。(MDN)

setTimeout 的第二个参数设置为 0 (未设置、小于 0、大于 2147483647 时都默认为 0)的时候,意味着马上执行,或者尽快执行。

setTimeout(function () {  console.log("你猜它什么时候打印?")}, 2147483648);

把这段代码放到浏览器控制台执行,你会发现它会立马打印出 你猜它什么时候打印?

最小延时>=4ms(嵌套应用定时器)

在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小距离是4ms,这通常是因为函数嵌套导致(嵌套层级达到肯定深度),或者是因为曾经执行的setInterval的回调函数阻塞导致的。

  • setTimeout 的第二个参数设置为 0 (未设置、小于 0、大于 2147483647 时都默认为 0)的时候,意味着马上执行,或者尽快执行。
  • 如果延迟时间小于 0,则会把延迟时间设置为 0。如果定时器嵌套 5 次以上并且延迟时间小于 4ms,则会把延迟时间设置为 4ms
function cb() { f(); setTimeout(cb, 0); }setTimeout(cb, 0);

在Chrome 和 Firefox中, 定时器的第5次调用被阻塞了;在Safari是在第6次;Edge是在第3次。所以前面的定时器都起码被提早了4ms

未被激活的tabs的定时最小提早>=1000ms

浏览器为了优化后盾tab的加载损耗(以及升高耗电量),在未被激活的tab中定时器的最小延时限度为1S(1000ms)。

let num = 100;function setTime() {  // 以后秒执行的计时  console.log(`以后秒数:${new Date().getSeconds()} - 执行次数:${100-num}`);  num ? num-- && setTimeout(() => setTime(), 50) : "";}setTime();

这里我在39秒时切到了其余标签页,咱们会发现它前面的执行距离都是1秒执行一次,并不是咱们设定的50ms。

setInterval的解决时长不能比设定的距离长

setInterval的解决时长不能比设定的距离长,否则setInterval将会没有距离的反复执行

然而对这个问题,很多状况下,咱们并不能清晰的把控处理程序所耗费的时长,为了可能依照肯定的距离周期性的触发定时器,咱们能够应用setTimeout来代替setInterval执行。

setTimeout(function fn(){  // todo  setTimeout(fn,10)    // 执行完处理程序的内容后,在开端再距离10毫秒来调用该程序,这样就能保障肯定是10毫秒的周期调用,这里工夫按本人的需要来写},10)

解决方案

办法一:requestAnimationFrame

window.requestAnimationFrame() 通知浏览器——你心愿执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该办法须要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,现实状态下回调函数执行次数通常是每秒60次(也就是咱们所说的60fsp),也就是每16.7ms 执行一次,然而并不一定保障为 16.7 ms。
const t = Date.now()function mySetTimeout (cb, delay) {  let startTime = Date.now()  loop()  function loop () {    if (Date.now() - startTime >= delay) {      cb();      return;    }    requestAnimationFrame(loop)  }}mySetTimeout(()=>console.log('mySetTimeout' ,Date.now()-t),2000) //2005setTimeout(()=>console.log('SetTimeout' ,Date.now()-t),2000) // 2002

这种计划看起来像是减少了误差,这是因为requestAnimationFrame每16.7ms 执行一次,因而它不适用于距离很小的定时器修改。

办法二: Web Worker

Web Worker为Web内容在后盾线程中运行脚本提供了一种简略的办法。线程能够执行工作而不烦扰用户界面。此外,他们能够应用XMLHttpRequest执行 I/O (只管responseXMLchannel属性总是为空)。一旦创立, 一个worker 能够将音讯发送到创立它的JavaScript代码, 通过将音讯公布到该代码指定的事件处理程序(反之亦然)。

Web Worker 的作用就是为 JavaScript 发明多线程环境,容许主线程创立 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后盾运行,两者互不烦扰。等到 Worker 线程实现计算工作,再把后果返回给主线程。这样的益处是,一些计算密集型或高提早的工作,被 Worker 线程累赘了,主线程不会被阻塞或拖慢。

// index.jslet count = 0;//耗时工作setInterval(function(){  let i = 0;  while(i++ < 100000000);}, 0);// worker let worker = new Worker('./worker.js')
// worker.jslet startTime = new Date().getTime();let count = 0;setInterval(function(){    count++;    console.log(count + ' --- ' + (new Date().getTime() - (startTime + count * 1000)));}, 1000);

这种计划体验整体上来说还是比拟好的,既能较大水平修改计时器也不影响主过程工作

总结

因为js的单线程个性,所以会有事件排队、先进先出、setInterval调用被废除、定时器无奈保障准时执行回调函数以及呈现setInterval的间断执行。

举荐浏览

介绍回流与重绘(Reflow & Repaint),以及如何进行优化?\
这些浏览器面试题,看看你能答复几个?\
这一次带你彻底理解前端本地存储\
面试官:说一说前端路由与后端路由的区别\
JavaScript之原型与原型链\
Javascript深刻之作用域与闭包\
this指向与call,apply,bind

感觉文章不错,能够点个赞呀^_^ 另外欢送关注留言交换~

关注公众号,获取更多精选文章~