深入理解JavaScript定时器

对于浏览器内部,大部分操作都是异步的生成事件并添加到JavaScript引擎线程的队列中,然后由JavaScript引擎线程进行调度执行。因此浏览器的很多事件都是和JavaScript相结合的,但是也有一些内部的限制。

首先我们非常确定JavaScript是单线程的,对于浏览器来说,一个窗体中只有一个JavaScript引擎线程。而其他的行为,如:渲染、下载等是由单独的线程进行管理的,且具有不同的优先级。

异步事件

前面提到大多数事件都是异步的,触发的时候就将回调函数添加到事件队列。浏览器提供了一个内部的回路,也就是之前所谈到的Event Loop,由它来负责检查队列和处理事件、执行函数等。详细可参考我的前一篇博文。而setTimeoutsetInterval也是将其需要执行的函数添加到事件队列。

事实上,大多数交互和活动都得通过事件循环。

事件重叠

一些情况下,会有多个事件在同一时间附加到事件队列里。

比如,click事件就会产生两个额外的事件:mousedownmouseup。其中,mouseupclick事件会同时被添加到事件队列;而mousedown事件则很有可能会和另外一个事件重叠:focus

setTimeout(func, 0)奇巧淫技

再一次解释关于0ms的误解:如果当前时钟周期内执行队列空闲,则立即执行该定时器,将回调函数加入到事件队列;然后等待下一个时钟周期,再执行该回调函数。不妨来看看下面的测试。

这段代码在我的浏览器中执行结果如下:

在我本地的Nodejs环境中执行结果如下:

上面的这个测试只是想说明setTimeout(func, 0)定时任务的回调函数执行时间是有延迟的,而并不是所谓的立即执行。

因此,我们可以利用setTimeout(func, 0)来解决事件重叠所产生的负面效果,修正执行顺序。

奇巧淫技之一:模拟浏览器的事件捕获

众所周知,浏览器的DOM事件都是采用冒泡的方式,只有个别浏览器是支持事件捕获的。而在实际的开发过程中可能存在需要事件捕获的需求,要求子元素的事件在父元素触发之后才能触发。为了兼容各个浏览器,我们不能使用事件捕获,而setTimeout(func, 0)在这个时候就很乐意帮忙了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<input type="button" value="click" id="cbtn">
<div id="result"></div>
<script type="text/javascript">
var cbtn = document.getElementById('cbtn')
, result = document.getElementById('result');

cbtn.onclick = function(e) {
setTimeout(function() {
result.innerHTML += 'input click, ';
}, 0);
};

document.body.onclick = function(e) {
result.innerHTML += 'body click -> ';
};
</script>


点击查看运行效果:

奇巧淫技之二:让浏览器更好的工作

大多数情况下,我们可以在浏览器的默认行为之前对事件进行处理,但是有时我们按照常规的思路去做的时候,往往事与愿违。比如下面的例子。

1
2
3
4
5
6
7
<input type="text" id="wordInput">
<script type="text/javascript">
var wordInput = document.getElementById('wordInput');
wordInput.onkeypress = function(e) {
this.value = this.value.toUpperCase();
};
</script>


看似一个很简单的需求:每输入一个字符,就将其转换为大写。但是上面的代码完全没有按照指示去做,不信你试试看:

如果没有下一次输入,文本框中的小写字母永远都不会转换为大写。Why? 因为浏览器在keypress事件处理的时候,还没有将我们输入的值添加到文本框。于是乎换一个事件来handle然后再处理吧,既然键按下的时候还木有值,那就等键弹起来之后再处理。

1
2
3
4
5
6
7
<input type="text" id="wordInput">
<script type="text/javascript">
var wordInput = document.getElementById('wordInput');
wordInput.onkeyup = function(e) {
this.value = this.value.toUpperCase();
};
</script>


运行试试吧。

大概似乎是可行了,可是仔细观察就看出问题了。keyup事件触发时,文本框已经具备完整的值了,但先是一个小写的值,键完全释放之后转变为大写。这不科学…这太丑陋…

是时候关门放出setTimeout(func, 0)了。。。

1
2
3
4
5
6
7
8
9
10
<input type="text" id="wordInput">
<script type="text/javascript">
var wordInput = document.getElementById('wordInput');
wordInput.onkeypress = function(e) {
var self = this;
setTimeout(function() {
self.value = self.value.toUpperCase();
}, 0);
};
</script>

已经完美了。keypress事件触发时,将转换大写的操作添加到事件队列,紧接着浏览器添加我们输入的值,然后近乎0延迟的执行我们的转换大写操作函数。

上面两个小案例只是冰山一角,so…合理利用setTimeout(func, 0),明天更美好!