JavaScript中的setTimeout函数

问题的提出

在解决Chromium的一个issue过程中,遇到了一个关于setTimeout(fn, 0) 的困惑。

oninstall = function() {
    console.log('install');
    setTimeout(function() {
        self.skipWaiting()
          .then(function() {
              console.log('skipWaiting resolved');
            });
      }, 0);
  };
onactivate = function() { console.log('activate'); };

这是一段Service worker的代码,用来接收主页面的Event(oninstall, onactivate…),这些状态的转换是Browser代码(C++)执行的,但是无论怎么修改代码,输出的结果总是install –> activate –> skipWaiting resolved,而不是期待的install –> skipWaiting resolved –> activate。经过研究,发现问题的关键正是在于setTimeout()函数的使用,然后顺藤摸瓜的深入了解了setTimeout()的原理。

问题的分析

我们知道JS在浏览器中的执行是单线程的,异步事件如(鼠标点击、定时器等)只有在CPU空闲时才执行。即setTimeout的准则是尽快执行,而不是立即执行

setTimeout()本质是给browser的event queue增加一个event,而此时渲染引擎(Rendering Engine)已经在这个queue里面,所以setTimeout()产生的event只会排在queue的后面,也会等待当前thread/process完成之后再执行。

setTimeout的深入研究

转自:
https://johnresig.com/blog/how-javascript-timers-work/
http://stormhouse.github.io/posts/2013/how-javascript-timers-work/

了解JavaScript定时器底层的工作原理是十分重要的。一般它们表现的不那么直观,是因为它在单独的一个线程中,所以它的行为表现的不很直观,甚至有些怪异。 以下三种方式可以让我们去创建并操作定时器:

  • var id = setTimeout(fn, delay); 用于起动一个定时器,经过给定的时间后调用特定的函数。该函数返回一个id,来取消这个定时器 。
  • var id = setInterval(fn, delay); 和setTimeout类似,间隔给定的时间来调用函数,直到被取消 clearInterval(id);
  • clearTimeout(id); 接收一个参数定时器函数id,用于取消定时器

为理解定时器内部如果工作,需要声明一个很重要的概念:定时器延时,并不可靠的。这是因为js在浏览器执行是单线程的,异步事件(如鼠标事件和定时器)只在当执行过程中有机会执行时(CPU空闲时)才执行。下图给了很好的解释。

这里写图片描述

(左侧为正常时序,右侧为定时器注册和发生顺序)。该图提供很多信息,帮助你完全理解JavaScript异步执行工作方式。这是一个一维图,垂直方向为时间轴,单位是毫秒。中间蓝色部分的表示一个个JavaScript代码执行块。例如,第一个js块执行了大约19毫秒…。

由于JavaScript在同一时间只能执行一段代码(原于它是单线程)所以这些代码块会阻塞其它异步事件的执行。意味着一个异步事件(如鼠标事件,定时器触发或ajax回调),它会被插入事件队列中排队等待执行(有一点很重要,在不同的浏览器中,这个队列模型是不同的,所以队列中的事件是如何触发的是不同的)

首先,在第一段js代码块中,两个定时器被初始化,一个10ms的setTimeout 和 一个10ms的setInterval。这个定时器启动实际上实在我们第一个js代码块完成之前,不过请注意,定时器所挂载的处理逻辑并没有立即被执行(由于线程模型是不能这样做的),而实际上,延时调用程序将会被插入队列,等待可调用时序时,被顺序执行。

其次,我们在第一个代码块中,我们触发了一次点击操作。这个异步事件相关的回调函数,和定时器一样,也不会立即被执行,同样进入队列等待执行。

当第一个Javascript代码块执行完成后,浏览器就会去问队列:接下来要执行什么?然而此时此刻,鼠标事件的句柄函数和定时器的延时调用函数都在等待。浏览器会在二者中选择一个(鼠标事件)立即执行。定时器的回调会等待下个时机,被按顺序调用。

注意图中,在鼠标事件的回调执行时,interval延时回调被执行了。但是需要注意的时,当interval再次被出发时(当一个定时器的延时处理在执行的时候),这时候程序的处理将会被丢弃。假设当有大块的代码正在执行时,你又有一堆的interval延时调用在排队,你希望结果很可能就是这个大块的js代码执行完毕后,interval的延时调用会一个接一个的被触发,而且在执行时没有延时时间,也就是会被连续的调用。可是相反,浏览器往往只是等待,直到没有更多的interval处理程序进行排队。

事实上,我们也可以看到,第三个interval回调触发的时候,这个interval本身也在执行中。这就像我们展示了一个很重要的现象就是:interval 并不在乎当前谁正在执行,他们不分青红皂白地将排队,即使这意味着回调之间的时间将被牺牲。

最后,当第二个interval回调执行完成后,我们能看到,对于js引擎来说,没有需要去执行的东东了。这就意味着,浏览器在等待新的异步事件发生了。到第50秒时,这个interval被再次触发,这时候没有东西在阻塞执行,因此他会被立即调用。

我们来用几行代码来更好的去分辨setInterval和setTimeout之间的区别:

setTimeout(function(){
    /* Some long block of code... */ 
    setTimeout(arguments.callee, 10); 
}, 10);

setInterval(function(){
    /* Some long block of code... */ 
}, 10);

这两段代码乍一看似乎差不多,但事实上相差很多。有一点值得注意的是,在这里面的setTimeout,两个回调执行的时间间隔至少会是10毫秒;而setInterval将尝试每10秒去执行一次,不去考虑上一次回调是否已经完成。

  1. Javascript是一个单线程执行的东东,迫使异步事件排队等待执行。
  2. setTimeout 与 setInterval执行代码的原理是完全不同的。
  3. 当一个定时器执行被阻塞时,他会等待下一个可能执行的时机去执行,所以这个延时可能会比预先设定的时间要长。
  4. 如果回调函数执行时间过长(长于定时器的延迟时间),“间隔定时器”有可能会一个接一个无间隔的执行。
var die = false; 
setTimeout(function() { 
    die = true; 
}, 100);

while(!die) { }

console.log('done');

如果你认为在100毫秒后,会打针done,说明你没有看懂此篇文章。你一定会觉得在100毫秒后,die的值变成true,然后console会被执行,如果你这样想那你就错了。记住setTimeout的准则是尽快执行,而不是立即执行。只有当主事件循环结束时,有时间片供setTimeout去执行时,定时器才会被执行。

参考资料:

https://stackoverflow.com/questions/779379/why-is-settimeoutfn-0-sometimes-useful

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值