放假的电话

setTimeout到底做了什么

对很多人来说,Javascript的单线程以及事件驱动,即使不是了解得非常透彻,但也绝对不会没听说过。setTimeout,更是在提到这2个概念时,经常会举到的例子。关于Javascript单线程的文章,网上一搜一大把,对我自己来说,可能没有什么写的意义。直到有一天,我知道了setTimeout(handler, 0)这个用法,觉得单单写一写setTimeout,似乎倒是个挺有意思的事情。

setTimeout最简单的用法,就像下面一样:

1
2
3
setTimeout(function() {
console.log('Hello World');
}, 1000);

这段代码会在 大约 1秒钟之后打印出Hello World。这里,之所以会用到 大约 2个字,是因为setTimeout所设置的时间并不是精确的,而是一个最小的等待时间。在上面的例子里,由于没有其它的任务在执行,输出Hello World的时间会非常接近1秒。但是,如果当前浏览器正在执行非常耗时Javascript代码,输出Hello World的时间可能会远远大于1秒。

在看过上面的例子后,再看下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setTimeout(function() {
console.log('t1');
}, 100)
setTimeout(function() {
console.log('t2');
}, 200)
var loopLength = 1000;
for(var i=0; i< loopLength; i++) {
console.log(1);
}
setTimeout(function() {
console.log('t3');
}, 50)

这些代码最后会输出什么呢?根据setTimeout设置的时间,似乎比较合理的是t3 t1 t2。但是,实际输出的结果,有可能是t1 t2 t3, t3 t1 t2或者t1 t3 t2!

对结果产生影响的,就是其中的那个for循环。根据loopLength设置的不同,for循环会执行不同的时间。由于Javascript是单线程的,在执行这个for循环的时候,没有办法正确响应这些setTimeout函数,导致了输出的不同。这里,就牵涉到一个问题,就是setTimeout到底干了什么?

根据HTML的定义规范,setTimeout实际会产生一个定时器,并会把这个定时器加入到一个由浏览器维护的队列里。在浏览器空闲的时候,它会检查这个定时器的队列,如果发现有某个定时器已经到了指定的等待时间,就会把相应的函数再加入Javascript的事件队列里。所谓的事件队列,就是一个由事件以及其对应的处理函数(handler)所构成的队列。

回到上面的例子

  1. 假设for循环执行的时间小于50ms,那么浏览器有足够的时间处理3个setTimeout函数,输出的便是t3 t1 t2
  2. 如果for循环的时间在50ms到100ms之间,那么在执行完代码后,由于第三个setTimeout已经满足了至少50ms的等待时间,会第一个输出,最后输出的结果也是t3 t1 t2
  3. 如果for循环时间在100ms到200ms之间,在执行完for循环后,第一个setTimeout已经满足了最小延迟的时间,会第一个输出,最后结果是`t1 t3 t2
  4. 如果for循环的时间大于200ms,那么执行完时,3个setTimeout都已经满足了最小等待时间,最后输出的结果是t1 t2 t3

上面的4点,如果仔细看的话,会发现其实有一个前提,就是先加入队列的定时器如果时间到了,会先被取出来加入时间队列。之前也提到了,浏览器会维护这个定时器的队列,但并没有规定说一定是一个先进先出的队列。怎么才能知道浏览器到底是怎么执行的呢?下面这段代码可以试着找出浏览器处理定时器队列的方法:

1
2
3
4
5
6
7
setTimeout(function() {
console.log('t1');
}, 100)
setTimeout(function() {
console.log('t2');
}, 100)

设定2个同样延时的定时器,会发现,在所有的浏览器里,都会输出t1 t2。可见,浏览器在维护定时器队列的时候,是先进先出的,这也比较符合直觉。

最后,再来看看setTimeout(handler, 0)这个用法,例如:

1
2
3
4
5
6
7
setTimeout(function() {
console.log('finish');
}, 0);
for ( ... ) {
....
}

有了之前的知识,可以知道,上面的代码,可以保证会在for循环后立即输出finish。执行的过程就是生成一个定时器,加入定时器队列。在浏览器忙于执行for循环的时候,并不会检查这个队列,直到执行完,会立即把定时器加入事件队列并处理,输出finish

题外话,即使定义了0延迟,其实也有一个规定的最小延迟4ms。在统一成4ms之前,最小的延迟是10ms。