在说起javascript中定时器的问题,需要事先明确这样几个概念:
1,setTimeout:setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式,
2,setInterval: setInterval()则是在每隔指定的毫秒数循环调用函数或表达式,直到clearInterval把它清除;
3,clearInterval: setInterval定时器在执行之后会返回一个对应的定时器ID,可以通过clearInterval(ID)删除定时器的执行
4,clearTimeout: set Timeout定时器在执行之后也会返回一个对应的定时器ID,通过clearTimeout就可以删除对应的定时器执行
JS中的事件循环机制机制:
浏览器(或者说JS引擎)执行JS的机制是基于事件循环。
由于JS是单线程,所以同一时间只能执行一个任务,其他任务就得排队,后续任务必须等到前一个任务结束才能开始执行。
同步任务直接在主线程队列中顺序执行,而异步任务会进入另一个任务队列,不会阻塞主线程。等到主线程队列空了(执行完了)的时候,就会去异步队列查询是否有可执行的异步任务了(异步任务通常进入异步队列之后还要等一些条件才能执行,如ajax请求、文件读写),如果某个异步任务可以执行了便加入主线程队列,以此循环。
在JS中,这个任务队列是一个包含各种消息的消息队列,这些消息就是注册异步事件的时候添加的回掉函数; 然而JS主线程只负责一件事情,那就是从消息队列中取消息,执行消息,在取消息,执行消息;当消息队列为空的时候,就会等待消息队列为非空的时候取消息,而且主线程只有在当前的消息执行完毕之后才会去取下一个消息执行;这种机制就叫做事件循环机制,
三、JS定时器的工作原理
(扣别人的)
在解释上面问题的答案之前我们先来了解一下定时器的工作原理,这里将用引用How JavaScript Timers Work中的例子来解释定时器的工作原理,该图为一个简单版的原理图。
上图中,左侧数字代表时间,单位毫秒;左侧文字代表某一个操作完成后,浏览器去询问当前队列中存在哪些正在等待执行的操作;蓝色方块表示正在执行的代码块;右侧文字代表在代码运行过程中,出现哪些异步事件。该图大致流程如下:
程序开始时,有一个JS代码块开始执行,执行时长约为18ms,在执行过程中有3个异步事件触发,其中包括一个setTimeout、鼠标点击事件、setInterval
第一个setTimeout先运行,延迟时间为10ms,稍后鼠标事件出现,浏览器在事件队列中插入点击的回调函数,稍后setInterval运行,10ms到达之后,setTimeout向事件队列中插入setTimeout的回调
当第一个代码块执行完成后,浏览器查看队列中有哪些事件在等待,他取出排在队列最前面的代码来执行
在浏览器处理鼠标点击回调时,setInterval再次检查到到达延迟时间,他将再次向事件队列中插入一个interval的回调,以后每隔指定的延迟时间之后都会向队列中插入一个回调
后面浏览器将在执行完当前队头的代码之后,将再次取出目前队头的事件来执行
这里只是对定时器的原理做一个简单版的描述,实际的处理过程比这个复杂。
定时器的用法:
setTimeOut用法
setTimeout函数的用法如下:
var timeoutID = window.setTimeout(func, [delay, param1, param2, ...]);
var timeoutID = window.setTimeout(code, [delay]);
timeoutID:定时器ID号,它可以在clearTimeout()函数中被用来清除定时器。
func:被执行的函数。
code:(替代的语法)一个被执行的代码串。
delay:延迟的时间,单位毫秒。如果没有指定,默认为0。
我们可以使用window.setTimeout或setTimeout,两个写法基本一样,只不过window.setTimeout将setTimeout函数作为全局window对象的一个属性来引用。
应用示例:
function timeout(){
document.getElementById('res').innerHTML=Math.floor(Math.random()*100 + 1);
}
setTimeout("timeout()",5000);
代码执行时,5秒后调用timeout()函数
setInterval用法
setInterval函数的参数及用法和setTimeout函数一样,请参照上文的setTimeout函数的用法介绍。不同的是,setInterval每隔一定的时间执行当中的func或code代码。
应用示例:
var tt = 10;
function timego(){
tt--;
document.getElementById("tt").innerHTML = tt;
if(tt==0){
window.location.href='/';
return false;
}
}
var timer = window.setInterval("timego()",1000);
函数timego()定义了页面元素#tt显示的内容,当tt等于0时,页面定向到首页。然后我们定义一个定时器timer,使用setInterval()每隔1秒调用一次timego()。这样timego会执行10次,每次数字tt会减1,直到为0。那么如果想停止定时器,可以使用以下代码:
window.clearInterval(timer);
setTimeout和setnterval之间的区别:
1,setTimeout在时间间隔之后将定时器插入到事件循环队列之中,若前面有执行时间较长的程序,可能会延迟执行;
2,set Interval按照时间间隔将定时器插入到循环队列之中,但是循环队列中某一时刻只能有一个定时器对象实例;因此在set Interval中存在以下两个问题:
(1)某些间隔会被跳过;
(2)多个定时器的代码执行之间的间隔可能比预期的小;
3, setTimeout和setInterval的作用只是把你要执行的代码在你设定的一个时间点插入到引擎维护的一个消息队列中, 插入消息队列并不意味着你的代码就会立马执行的,理解这一点很重要
对于set Timeout:
function click() {
// code block1...
setTimeout(function() {
// process ...
}, 200);
// code block2
}
假设我们给一个button的onclick事件绑定了此方法, 当我们按下按钮后, 肯定先执行block1的内容, 然后运行到setTimeout的地方, setTimeout会告诉浏览器说, “200ms后我会插一段要执行的代码给你的队列中“, 浏览器当然答应了(注意插入代码并不意味着立马执行), setTimeout代码运行后, 紧跟其后的block2代码开始执行, 这里就开始说明问题了, 如果block2的代码执行时间超过200ms, 那结果会是如何?
或许按照你之前的理解, 会理所当然的认为200ms一到, 你的process代码会立马执行…事实是, 在block2执行过程中(执行了200ms后)process代码被插入代码队列, 但一直要等click方法执行结束, 才会执行process代码段, 从代码队列上看process代码是在click后面的, 再加上js以单线程方式执行, 所以应该不难理解.
如果是另一种情况, block2代码执行的时间<200ms, setTimeout在200ms后将process代码插入到代码队列, 而那时执行线程可能已经处于空闲状态了(idle), 那结果就是200ms后, process代码插入队列就立马执行了, 就让你感觉200ms后, 就执行了.
再看看setInterval :它存在两个问题
(1)某些间隔会被跳过;
(2)多个定时器的代码执行之间的间隔可能比预期的小;
function click() {
// code block1...
setInterval(function() {
// process ...
}, 200);
// code block2
}
和上面一样我们假设通过一个click, 触发了setInterval以实现每隔一个时间段执行process代码
这里写图片描述
比如onclick要300ms执行完, block1代码执行完, 在5ms时执行setInterval, 以此为一个时间点, 在205ms时插入process代码, click代码顺利结束, process代码开始执行(相当于图中的timer code), 然而process代码也执行了一个比较长的时间, 超过了接下来一个插入时间点405ms, 这样代码队列后又插入了一份process代码, process继续执行着, 而且超过了605ms这个插入时间点,
下面问题来, 可能你还会认为代码队列后面又会继续插入一份process代码…真实的情况是,由于代码队列中已经有了一份未执行的process代码, 所以605ms这个插入时间点将会被“无情”的跳过, 因为js引擎只允许有一份未执行的process代码, 这里我想我们都明白了JS引擎的小九九了吧…
4,内存泄漏问题:
内联书写setInterval时,由于匿名函数被定义于全局中,不能够计时器的清除,因此很容易造成内存泄露。
规避办法:
setTimeout(function(){
// 其他代码
setTimeout(arguments.callee, interval);
}, interval);
// 备注:interval指的是毫秒数。
// 基本原理是:链式的调用setTimeout,每次函数执行时会创建一个新的定时器,第二个setTimeout调用使用了arguments.callee来获取对当前执行的函数的引用,并为它设置另一个定时器。这样做的好处是,在前一个定时器代码执行完毕之后,不会在队列中加入新的定时器代码,同时还能够没有任何缺失的间隔,从而避免连续运行。callee 属性是 arguments 对象的一个成员,他表示对函数对象本身的引用,这有利于匿名函数的递归或确保函数的封装性