javascript中定时器是如何工作的
计时器(定时器)是一个大家常常不能够充分理解,并且常常被误用的JavaScript的特性之一,使用计时器能够开发非常复杂的动态效果,它具有在一段时间后异步执行一段代码的能力,由于JavaScript天生是单线程的,但是使用计时器能够打到异步执行一段代码的能力。
从根本上理解定时器是如何工作的是非常重要的,由于它是单线程运作,定时器如何运作并不是太明了,下面我们通过一下三个方法,来看看它到底是如何执行的,这三个方法能够形成和操作计时器:
var id = setTimeout(fn, delay);该方法初始化一个单独的计时器,用于在delay指定的时间后调用fn函数,该方法会产生一个唯一的ID,稍后可以使用该ID取消该计时器
var id = setInterval(fn, delay); 该方法和上面的方法类似,他会在每隔delay指定的时间后都会都会执行fn函数,也会产生一个唯一的ID,用于取消定时器。
clearInterval(id);, clearTimout(id);接受上述两个函数返回的ID,停止定时器调用上面两个函数中的第一个参数。
为了理解定义器内部是如何工作的,有一个重要的概念我们需要理解:定时器延迟时间并不能保证。因为所有的JavaScript在浏览器中执行一个单线程的异步事件时,只有当有一个入口时才能开始执行。
通过下图,我们能够更好的理解这个原因:
在这个示例中有很多信息可以挖掘,但是完全理解了之后你将会更清楚地认识到异步的JavaScript是怎么执行的。这是个一维的图:竖直方向上的是(挂钟式)时间,单位为毫秒。蓝色的框表示正在执行的JavaScript片段。举例来说,第一块JavaScript执行了约18ms,而鼠标点击则执行了约11ms,以此类推。
由于JavaScript向来都只能在同一时间执行一块代码(这是由它单线程的本质决定的),所以每一个代码块都“阻塞”了其他的异步事件。这意味着当异步事件发生时(比如鼠标点击、timer触发或者是XMLHttpRequest完成),这些事件将进入到一个队列中等待执行(队列的实现方法因浏览器而异,我们在此只讨论一个简化的情况)。
首先,在第一个JavaScript块中,有两个timer被初始化了:一个10ms的setTimeout和一个是10ms的setInterval。由于timer(这里的timer指setTimeout中的 timer,而下文中的interval则指setInvertal中的timer)开始的时间,实际上它在第一个代码块结束前就已经触发了。然而请注意,它并不会马上执行(事实上由于单线程的存在,它也无法做到马上执行)。相反的,这个被延期执行的函数进入队列中,等待在空闲的时候被执行。
在第一个JavaScript块中,我们看到一个鼠标点击事件也发生了。而与这个异步事件(我们不知道用户什么时候会去执行一个动作,因此将其认为是一个异步动作)相关的JavaScript回调函数也无法立马执行,正如timer一样,它也进行到队列中等待被执行。
当第一个JavaScript块被执行完之后,浏览器问了一个问题:有正在等待被执行的代码吗?在这个例子中,鼠标点击事件和time事件都正在队列中等待。于是浏览器选了一个(鼠
标点击事件),然后马上执行它。而timer只能继续等下去。
注意当鼠标点击事件正在执行的时候第一次的interval事件也触发了,与timer一样,它的事件也进入队列等待之后执行。然而,注意,当interval再次触发的时候(这个时候timer的事件正在执行),这一次它的事件被丢弃了。如果你在一个大的JavaScript代码块正在执行的时候把所有的interval回调函数都囤起来的话,其结果就是在JavaScript代码块执行完了之后会有一堆的interval事件被执行,而执行过程中不会有间隔。因此,取代的作法是浏览器情愿先等一等,以确保在一个interval进入队列的时候队列中没有别的interval。
事实上,我们可以在例子中看出:当第三个interval触发的时候这个interval自身正在执行。这告诉我们一个重要的事实:interval是不管当前在执行些什么的,在任何情况下它都会进入到队列中去,即使这样意味着每次回调之间的时间就不准确了。
最后,当第二个interval回调执行完后,我们可以看到队列已经被清空,没有什么需要JavaScript引擎去执行的了。这表明浏览器现在等待一个新的异步事件发生。于是在50ms的
时候我们看到interval又触发了。这一样,由于没有什么东西挡住了它的执行,它马上就触发了。
让我们来看一个例子,这个例子更好地阐释了setTimeout和setInveral之间的区别。
setTimeout(function(){
/* 一个很长的代码块…… */
setTimeout(arguments.callee, 10);
}, 10);
setInterval(function(){
/* 一个很长的代码块…… */
}, 10);
乍看上去,这两段代码在功能上似乎是相同的,可实际上并非如此。setTimeout的代码在前一次的回调执行完后总是至少会有10ms的延时(有可能会更多,但是绝对不会更少);而setInterval则总是在每10ms的时候尝试执行一次回调,它不管上一次回调是什么时候执行的。
我们在此学到了很多,让我们重述一下:
* JavaScript引擎只有一个线程,这使得异步事件必需列队等待执行。
* setTimeout和setInterval在如何执行代码上有着本质地区别。
* 如果一个timer在将要执行的时候被阻塞,它将会等待下一个时机(比预期的延时长)。
* 如果interval的执行时间较长(比指定的延时长),那么它们将连续地执行而没有延时。
什么地方使用JavaScript定时器
- 大量处理dom时,我们可以采用分步加载的办法。
我们通过下面的代码理解这种用法:
var table = document.getElementsByTagName("tbody");
for ( var i = 0; i < 2000; i++ ) {
var tr = document.createElement("tr");
for ( var t = 0; t < 6; t++ ){
var td = document.createElement("td");
td.appendChild( document.createTextNode(" "+t ));
tr.appendChild( td );
}
table[0].appendChild( tr );
}
//如果我们采用分步加载呢;
var i = 0, max = 1999;
setTimeout(function(){
for ( var step = i + 500; i < step; i++ ) {
var tr = document.createElement("tr");
for ( var t = 0; t < 6; t++ ){
var td = document.createElement("td");
td.appendChild( document.createTextNode("" + t));
tr.appendChild( td );
}
table[0].appendChild( tr );
}
if ( i < max )
setTimeout( arguments.callee, 0 );
}, 0);
- 中央定时器控制
当大量使用定时器时,我们该如何管理这些定时器,这种情况在动画处理中尤其重要,因为需要同时处理大量的属性,所以需要一种方案管理这些。
var timers = {
timerID: 0,
timers: [],
start: function(){
if ( this.timerID )
return;
(function(){
for ( var i = 0; i < timers.timers.length; i++ )
if ( timers.timers[i]() === false ) {
timers.timers.splice(i, 1);
i--;
}
timers.timerID = setTimeout( arguments.callee, 100 );
})();
},
stop: function(){
clearTimeout( this.timerID );
this.timerID = 0;
},
add: function(fn){
this.timers.push( fn );
this.start();
}
};
通过使用这样一个定时器,我们可以了解到一下一个好处:
1.在同意时间一个页面上只有一个定时器在执行。
2.你可以根据你的意志暂停或者重新开始定时器。
3.你可以频繁的处理删除的回调函数。
下面来看看如果使用这个中央定时控制器:
<div id="box" style="position:absolute;" mce_style="position:absolute;">Hello!</div>
var d=document,box = d.getElementById("box"), left = 0, top = 20;
使用的时候只需要:
timers.add(function(){
box.style.left = left + "px";
if ( ++left > 50 )
return false;
});
timers.add(function(){
box.style.top = top + "px";
top += 2;
if ( top > 120 )
return false;
});
};