JavaScript定时器与执行机制

  JavaScript动画中是必须使用到定时器的,这里做一个总结。 

var label = 'someLable';

console.time(label);

console.timeEnd(label);

    通过上面的代码,我们可以进行时间统计。

  

从JS执行机制说起(任务队列)

  首先,需要知道的是无论是否提到了异步,JavaScript都是单线程的(注意:这里的单线程并不是真正意义上的单线程,我们所说js单线程,是说js的代码执行只有一个线程,但是比如js使用过程中,会用到一些I/O操作,这些还是需要I/O线程的。所以,这里的单线程是执行代码始终只有一个线程。),作为脚本语言,也不允许其出现多线程,否则,对于DOM的操作就会引起混乱,当然,现在出现的web worker确实提供了JavaScript创建多个线程的能力,但是子线程是完全收到主线程控制的,并且不得操作DOM。 所以,这个新标准没有改变JavaScript单线程的本质。

  既然是单线程那么同一时间内只能执行一个任务,其他任务就得排队,后续任务必须等到前一个任务执行完才能开始执行。

为了避免因为某些长时间任务造成的无意义的等待,js引入了异步的概念

OK! 那么究竟什么是无意义的等待呢? 我们应该怎么理解呢?

  • 比如读取文件的时候,这是cpu几乎是没有占用的,但是读取文件在耗费大量的时间,如果不采取异步的方式,那么cpu的利用率就会很低。 即异步操作的大部分时间cpu是帮不上忙的。 
  • 比如发送ajax请求的时候,如果采用同步,那么cpu就需要一直等着他,如果网络不好,可能就要等很久,并且cpu是帮不了忙的,因此采用异步,等到他做完了,通知cpu就行了啊,或者cpu主动去找也行啊。
  • 就像之前说过了同步、异步单线程、异步多线程的区别是一样的。 你(这里的你就是cpu)是一个厨师,分给你煮鸡蛋和做烤面包的任务。如果同步,就是先煮鸡蛋,然后在锅旁边等着它煮熟,你什么都不做,但是这是cpu也没有用上啊。 那么异步单线程呢? 这时,你(cpu)可以先煮鸡蛋,然后设置一个定时器(比如煮水好了,就会发出尖叫声),然后去烤面包,也设置一个定时器(方式不重要,反正一定是会提醒的)。 这时候你就可以去做其他事情了(cpu在主线程上执行后续任务),而不用等着那些你帮不上忙的事情。等到提醒你(cpu)都煮熟的时候,(cpu知道了就会将之加入主线程处理),你再拿着这些东西送给客人。   而异步多线程呢?这个也是很好理解的,就是你(cpu)可以多雇佣两个人(两个线程),一个人负责煮鸡蛋(只看着煮鸡蛋,别的什么都不做),另外一个人负责烤面包(只烤面包,其他的什么都不做),你负责协调。 嗯。 
  • 同步任务会直接在主线程中顺序执行。 
  • 异步任务需要先排到一个任务队列中去,不会阻塞主线程。等到主线程执行完了之后,就会去查询在异步的任务队列中是否有可以执行的任务(异步可能还需要等待一些ajax请求,文件读写),一旦这些异步任务可以执行了,就会将它添加到主线程队列中,以此循环。 

 注意: 这里所说的任务队列一定是和异步任务有关的,而同步任务只会在主线程中排队执行,不会涉及到任务队列的概念

  


事件和回调函数

  “任务队列”是一个事件的队列(也可以理解为消息的队列)IO设备完成了一项任务,就在“任务队列”中添加一个事件,表示相关的异步任务可以进入主线程(执行栈)了,主线程读取任务队列,就是读取里面有哪些事件。  

  “任务队列”中的事件,除了IO设备的事件之外,还包括了一些用户产生的事件(比如鼠标点击、页面滚动等),这些事件都会添加到任务队列中去。注意:前提条件是这些事件都有相应的回调函数,其他的异步也是如此,有相应的回调函数,然后有事件才能被添加到任务队列中区)。   

  所谓回调函数, 就是那些被主线程挂起的代码(不会被理解执行),异步任务必须制定回调函数,当主线程开始执行异步任务时,就是执行对应的回调函数。 

  “任务队列”是一个先进先出的数据结构,排在前面的事件,就优先被主线程读取。 主线程的读取过程基本上是自动的,只要执行栈一清空,“任务队列”上第一位的事件就会自动进入主进程。 但是,由于存在后文提到的“定时器”功能,主线程要先检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程(这些时间就是我们所说的定时器)。

  

  


Event Loop

   由于主线程从“任务队列”中读取事件的这个过程是不断循环的,所以整个的这种运行机制又称为 Event Loop(事件循环)。 

   主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done), 显然,任务队列中有各种事件,也都是主线程在调用api的时候放进去的。只要栈中的代码执行完毕(关键,一定要是等到栈中的代码执行完毕),主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。


定时器

  JS的定时器目前有三个:setTimeout、setInterval和setImmediate。

  定时器也是一种异步任务,通常浏览器都有一个独立的定时器模块,定时器的延迟时间由定时器模块来管理, 当某个定时器到了可执行状态,就会被加入到主线程之中。

  

setTimeout

  setTimeout(fn, x)表示延迟x毫秒之后执行fn,但是使用的时候,千万不要太相信预期,延迟的时间严格来说总是大于x毫秒的,至于大多少,就要看当时JavaScript的执行情况了。

  另外,如果多个定时器如果没有及时清除,就会存在干扰,总之,及时清除已经不需要的定时器是一个好的习惯。

HTML5规范规定了最小延迟时间不得小于4ms,即如果x小于4,会被当做4来处理。

   setTimeout注册的函数fn会交给浏览器的定时模块来处理,延迟时间到了就会添加fn这个回调函数到主进程队列当中,但是如果主进程队列前面刚好还有正在执行的没有执行完的代码,则又要花费一定的时间去等待主进程,然后再执行fn,所以实际的时间往往是更长的。

   如果fn之前在主进程中刚好有一个超级大的循环,那么延迟就不是一点了: 

(function testSetTimeout() {
    const label = 'setTimeout';
    console.time(label);
    setTimeout(() => {
        console.timeEnd(label);
    }, 10);
    for(let i = 0; i < 10000000000; i++) {}
})();

  最后执行结果如下:

setTimeout: 12131.85986328125ms

  即在函数中虽然我们设置了在10ms之后执行函数,但是实际上却是 12131ms,近千倍的差距就是因为主进程上还有没有执行完的任务。下面我们进行相应的解析:

  • 首先在主进程上同步执行 第一句、第二句代码
  •  接着,遇到了setTimeout函数,异步执行。即首先将异步执行的回调函数和这个事件放在任务队列里,然后将时间告诉给浏览器的定时模块。 
  • 然后迅速开始执行下面的for循环,由于这个循环很大,所以执行很久。在这个过程中, 浏览器的定时模块在10ms之后就开始把这个回调函数放在了主线程的执行队列中。
  • 但是主线程上的for还没有执行完毕,所以,回调函数就需要一直等着,知道for执行完毕,主线程队列上的回调函数开始执行。 所以时间有了一个很大的延迟。
      function fda() {
          for (var i = 0; i < 999; i++) {
              console.log(4);
              setTimeout(function () {
                console.log(5);
              }, 10);
          }
      }
      fda();

  比如上面这段代码,就是先输出所有的4,然后再输出所有的5, 因为主线程的任务没有执行完,放在任务队列中的回调函数也就不会立即执行。

setInterval

  setInterval的实现机制跟setTimeout类似,只不过setInterval是重复执行的。

  对于setInterval(fn, 100)容易产生一个误区: 执行完fn之后的100ms立即再次执行fn。 

  事实上,setInterval交给了浏览器的定时模块,定时模块并不会顾及fn的上一次的执行结果,而是每隔100ms就把fn放在主线程上,但是主线程上是否有任务在执行而不让fn执行呢 ?这个浏览器的定时模块就不管了,我们也无法确定。 

  

  

setImmediate

  这算一个比较新的定时器,目前IE11/Edge支持、Nodejs支持,Chrome不支持,其他浏览器未测试。

  这个api的支持性并不是很好。

    从API名字来看很容易联想到setTimeout(0),不过setImmediate应该算是setTimeout(0)的替代版。

  在IE11/Edge中,setImmediate延迟可以在1ms以内,而setTimeout有最低4ms的延迟,所以setImmediate比setTimeout(0)更早执行回调函数。不过在Nodejs中,两者谁先执行都有可能,原因是Nodejs的事件循环和浏览器的略有差异。

  很明显,setImmediate设计来是为保证让代码在下一次事件循环执行,以前setTimeout(0)这种不可靠的方式可以丢掉了

  总之,记住setImmediate会比setTimeout(fn, 0)更快、更及时一点就么有错了。

  

  

requestAnimationFrame

  requestAnimationFrame并不是定时器,但和setTimeout很相似,在没有requestAnimationFrame的浏览器一般都是用setTimeout模拟。

  requestAnimationFrame跟屏幕刷新同步,大多数屏幕的刷新频率都是60Hz,对应的requestAnimationFrame大概每隔16.7ms触发一次,如果屏幕刷新频率更高,requestAnimationFrame也会更快触发。基于这点,在支持requestAnimationFrame的浏览器还使用setTimeout做动画显然是不明智的。

  这一点很关键,requestAnimationFrame是跟着屏幕来刷新的,而不会顾及到任务队列的事情。所以会更为及时。 

  在不支持requestAnimationFrame的浏览器,如果使用setTimeout/setInterval来做动画,最佳延迟时间也是16.7ms。 如果太小,很可能连续两次或者多次修改dom才一次屏幕刷新,这样就会丢帧,动画就会卡;如果太大,显而易见也会有卡顿的感觉。

  所以,我们最好就要设置为16.7ms,如果设置的少了,还有可能出现问题,何必呢?

  

  有趣的是,第一次触发requestAnimationFrame的时机在不同浏览器也存在差异,Edge中,大概16.7ms之后触发,而Chrome则立即触发,跟setImmediate差不多。按理说Edge的实现似乎更符合常理。

Promise

  

  Promise是很常用的一种异步模型,如果我们想让代码在下一个事件循环执行,可以选择使用setTimeout(0)、setImmediate、requestAnimationFrame(Chrome)和Promise。

而且Promise的延迟比setImmediate更低,意味着Promise比setImmediate先执行。

function testSetImmediate() {
    const label = 'setImmediate';
    console.time(label);
 
    setImmediate(() => {
        console.timeEnd(label);
    });
}
 
function testPromise() {
    const label = 'Promise';
    console.time(label);
    new Promise((resolve, reject) => {
        resolve();
    }).then(() => {
        console.timeEnd(label);
    });
}
 
testSetImmediate();
testPromise();

  

  

process.nextTick

  

  process.nextTick是Nodejs的API,比Promise更早执行。

  事实上,process.nextTick是不会进入异步队列的而是直接在主线程队列尾强插一个任务虽然不会阻塞主线程,但是会阻塞异步任务的执行,如果有嵌套的process.nextTick,那异步任务就永远没机会被执行到了。

使用的时候要格外小心,除非你的代码明确要在本次事件循环结束之前执行,否则使用setImmediate或者Promise更保险。

  

原文地址:https://www.cnblogs.com/zhuzhenwei918/p/7413014.html