深入setTimeout和setInterval

Author : lovecicy

在JS中,我们经常需要使用到延时执行的情况,能实现这个功能的JS方法有两个,分别是setTimeoutsetInterval。相信这两个方法大家一定经常用到,那么今天我们就来八一八这两个方法吧,可不要觉得这两个方法很简单哦,看完以后相信你一定有所收获。

一、定义

虽然这里我们只放了setTimeoutsetInterval两个方法,其实还有两个与之对应的清除延时执行的方法:clearTimeoutclearInterval,它们两个接收一个ID,分别由setTimtoutsetInterval方法返回。

接下来我们来看看Mozilla开发者网站给出的定义:

setTimeout: Calls a function or executes a code snippet after a specified delay.
setInterval: Calls a function or executes a code snippet repeatedly, with a fixed time delay between each call to that function. Returns an intervalID.

区别在于setInterval是重复执行,而setTimeout只会被执行一次。

二、语法

var timeoutID = window.setTimeout(func, delay, [param1, param2, ...]); 
var timeoutID = window.setTimeout(code, delay);

var intervalID = window.setInterval(func, delay[, param1, param2, ...]); 
var intervalID = window.setInterval(code, delay);

参数:

  1. func: 要执行的函数(注意这里要求的是函数,而不是函数执行的结果,传入funcfunc()可是大大的不同哦)
  2. code: 字符串,表示要执行的代码(不推荐,里有同eval()
  3. delay: 延迟的毫秒数(实际的延时可能会更久)
  4. [param1, param2, …]: func的参数列表,IE9以下不支持

返回值:

timeoutID/intervalID 代表当前延时任务的ID,是个数字,可以通过clearTimeout(timeoutID)/clearInterval(intervalID)来清除延时任务。

三、区别

简单的说,它们之间的区别就是:setTimeout只执行一次,而setInterval会重复执行,直到被清除。而要让setTimeout实现重复执行的话,只需要在一个函数内延时调用自己即可,所以一般来说,能用setInterval的地方都能用setTimeout代替。

但是,更重要的却别在于这两种方法实现重复执行的区别,来看下面的代码:

function long(){
  var t = 0;
  for(var i=0; i<1000; i++){
    for(var j=0; j<1000; j++){
      t ++;
    }
  }
  console.log(t);
}
function longCopy(){
  console.time('timer');
  var t = 0;
  for(var i=0; i<1000; i++){
    for(var j=0; j<1000; j++){
      t ++;
    }
  }
  console.timeEnd('timer');
  console.log(t);
}
function test(){
  long();
  setTimeout(test, 5);// long执行完以后,过5毫秒再执行test
}
test()
setInterval(long, 5); // 每5毫秒执行一次long,不管上次调用的long有没有执行完毕

执行longCopy()我们可以看到console里输出:’timer: 5.000ms’。也就是说long()方法执行需要5毫秒。当我们执行test()时,每10毫秒输出计算的结果100000;而通过setInterval(long, 5);的方式执行,首先经过5毫秒输出100000,接着持续输出100000,中间没有时间间隔。

当然,这断代码是死循环,但是想要说明的问题就是:setTimeout不能保证我们的代码按照规定的延时执行,但是它能确保后一次循环代码在当前代码之后间隔一段时间再执行。如果我们要定时向服务器发起AJAX请求,setTimeout能保证我们的请求按顺序返回。这里涉及到JavaScript的定时机制,我们稍后会介绍。总之,结论就是,用setTimeout实现循环时,我们能保证每次循环间隔时间是预期的间隔;而用setInterval则不能。因此,请尽量使用setTimeout代替setInterval

四、注意

明白了setTimeoutsetInterval的区别,让我们再来看看在使用它们时,需要注意哪些情况。

1. this指向

this的指向问题,初学者很容易被绕晕,就算是从事前端工作好多年的老人也偶尔还会被this指向问题所困扰,而关于这个,可以参见之前的一篇博文

同样的,在setTimeoutsetInterval中,传入的函数的this指向也需要注意,先来看一段代码:

var obj = {
  prop: function(){
    console.log(this);
  }
}
obj.prop(); //这里的this指向obj
setTimeout(obj.prop, 100); //输出window对象

可以看到,通过setTimeout执行obj.prop方法,其中的this指向的是window对象而非obj对象。在使用setTimeout/setInterval时,千万要注意。

2. 传参

在大多数情况下,我们的函数都需要传入参数,在现代浏览器中,在我们传给setTimeout/setInterval的参数中,从第3个参数开始的参数都将作为参数传给我们要延时执行的函数,但是不幸的是,在IE9以前,你不能这么干,那么如何实现传参的目的,改如何实现呢?答案是闭包

function wrapper(delay, param1, param2, param3){
  function real(){
    //you can access params here
  }
  setTimeout(real, delay);
}

wrapper(1000, 'a', 'b', 'c');

当执行setTimeout的时候,我们可以理解为在wrapper外部调用了real(),这样就形成了闭包,通过把参数传给外层的wrapper,我们就能在real内部访问到传给wrapper()的参数,easy,hah!

3. 最大与最小延迟时间

在HTML5的规范中,最大和最小可设置的延时是2147483647ms(231-1)和4ms。

关于最大延时值,MDN给出的解释是:各个浏览器在内部都使用了一个32位的整数来存储这个延时值,因此任何大于2147483647的延时值会导致一个整数值的溢出,致使延时函数被立即执行。

而关于最小值,从2010年之后,各个浏览器都统一使用HTML5的标准,即4ms,但是测试之后却发现并不是如此,它的变化范围可以从1ms到10ms+。而在之前,每个浏览器采用不同的标准,大致是10ms。并且,看完第五部分后,你会知道setTimeout/setInterval的延时并不精确。

var t1;
function test(){
  t1= new Date().getTime();
  setTimeout('console.log(new Date().getTime()-t1)');
}
test(); //chrome 会输出4

4. 第一个参数问题

这里的第一个参数,有一些要注意的地方:

function normal() {
  console.log(3);
  return 3;
}
function returnString() {
  return 'console.log("test")';
}
function returnFunc() {
  return function(){
    console.log('in returned function');
  };
}
setTimeout(normal,10); //输出3
setTimeout(normal(),10); //也会输出3,但是不同于第一种情况,这里的3会被立即输出,没有延时
setTimeout(returnString,10); //没有输出
setTimeout(returnString(),10); //输出'test'
setTimeout(returnFunc,10); //没有输出
setTimeout(returnFunc(),10); //输出'in returned function'

总之,记住一句话:它只接受一个函数,或者字符串。当传入函数时,函数被调用,上面的例子中,1,3,5,6传入的都是函数,而4传入的不是函数,是函数的返回值,即字符串。

五、JavaScript Timer

最后,我们来看一看JS的计时器吧。

在JS中,我们可以通过setTimeout/setInterval/clearTimeout/clearInterval几个函数操作定时器。首先,有一个概念:在浏览器中,所有的JS代码都是在一个线程中被执行的;异步的事件,比如鼠标点击,延时事件都需要在线程空闲时才能被执行,它不会被多线程执行。下面的图很好地展示了上面的描述:

Timer

图中有大量的信息需要我们来消化,但是当你完全理解以后,你会对JS的异步执行有更好的理解。这是一张一维的图:纵向表示时间轴,以毫秒为单位。蓝色的盒子代表一段段被执行的JS代码。例如,第一部分的JS大约执行了18ms,鼠标点击事件的处理函数大约执行了11ms等。

让我们按照时间顺序,慢慢讲解这张图吧,右边的箭头代表发生的事情:

  1. 代码开始执行,当执行到大约3ms时,我们调用了setTimeout,延时为10ms,假设我们要延时执行的是A
  2. 鼠标点击事件发生
    按理说,事件发生了,与这个事件绑定的回调函数就应该被执行了。但是,我们发现当前的JS还没有执行完,由于JS的单线程缘故,这个回调函数不能被立即执行。
    然后,浏览器会将这次点击放入到一个事件处理队列中等待。我们可以将这个队列看成一个数组Q,现在Q有一个值了
  3. 在大约10ms时,我们调用了setInterval,延时也是10ms,假设延时执行B
  4. 到了大约13ms时,我们的setTimeout延时到了,该是执行A的时候了
    可是不巧,我们的第一段JS还没执行完,于是,浏览器又把这次延时事件放到了Q里,现在Q有两个值了
  5. 到了大约18ms,我们的第一段JS终于执行完了。JS线程空闲了,于是浏览器跟JS线程说,来来来,这儿还有两件事儿你要干的,喏,先去处理鼠标点击事件吧
  6. 于是紧接着,JS线程开始处理鼠标点击事件了。现在Q里面的第一个值被取走,只剩一个A要执行了
  7. 当20ms的时候,我们的JS线程还在处理鼠标点击事件,A还静静地呆在Q里等待被处理。这时候,在第三步被要求延时10ms执行的B也发现,时间到了,该处理我了。
    一上去,它就发现JS线程还没空处理它,于是浏览器又把它加入到队列Q里面了。现在我们的Q里面又有两个值了:A和B兄弟俩
  8. 又过了9ms,现在是浏览器时间29ms整,鼠标点击事件被处理完了。JS线程又空闲了,浏览器又说,来来来,这儿还有两件事,去处理A吧
  9. 可怜的JS线程没一刻空闲,又得开始处理A了,这时候,Q里又只剩下B一个值了
  10. 时间马上来到了30ms,JS线程刚开始处理A才1ms呢,第三步要求被延时处理的函数又发现时间到了,轮到我上场了,于是它又上了,这时候我们叫它B1吧
    可惜,JS线程还在干活,没空处理B1。那就到队列Q里等着吧。浏览器于是想把B1也放到Q里面,结果发现已经有一个B1的兄弟B在Q里了,这时候浏览器怎么选呢?
    浏览器是这么考虑的:假如有件事特费时,要干一小时,而我们的setInterval告诉它,每隔一分钟处理一次我。然后,在这一小时内,不停地有延时事件被
    加入队列Q里等待被执行,一个小时后,我的JS线程要马不停蹄地处理延时事件60次了,如果这些延时事件又特别费时,那处理这60件延时事件的同时,
    又会不停得有延时事件被加入Q,那JS线程不是别的啥事都别干了?
    于是,浏览器决定这么干,对于每一个setInterval,队列Q中同时只能存在一个延时事件
    所以当浏览器发现Q里已经有B了时,B1就被无情的抛弃了,是的,就是这样。
  11. 到了36ms左右,A被处理好了,这时候JS线程又有空处理B了
  12. 这时候,队列Q空了。等到40ms的时候,B2被添加到Q里
  13. 41ms左右,JS线程开始处理B2
  14. 在45ms左右,B2被处理完,队列Q也为空,JS线程终于可以休息了
  15. 到50ms的时候,因为JS线程有空,所以直接处理B3,之后每个10ms处理一次Bn,如果浏览器没有其他事情发生的话

到这里,我们已经知道了浏览器是如何执行JS的了,setTimeout/setInterval是不准时的,setTimeoutsetInterval是不一样的,JS Timer是如何工作的。

最后,我们来验证一点,队列Q里,是否可以有多个setInterval的存在:

function intervals(){
    function test1(){
        console.time('timer1');
        console.log('test1');
        clearInterval(cid1);
        console.timeEnd('timer1');
    }
    function test2(){
        console.time('timer2');
        console.log('test2');
        clearInterval(cid2);
        console.timeEnd('timer2');
    }
    var cid1= setInterval(test1,100);
    var cid2= setInterval(test2,120);
    console.time('timer');
    var t = 0;
    for(var i=0; i<2000; i++){
        for(var j=0; j<1000; j++){
            t ++;
        }
    }
    console.log(t)
    console.timeEnd('timer');
}
intervals();

可以发现,’test1’与’test2’都被输出,证明Q中可以存在多个setInterval的延时函数,而同一个setInterval延时函数则最多只能存在一个在Q中。

参考:
1. http://www.cnblogs.com/youxin/p/3354924.html
2. http://ejohn.org/blog/how-javascript-timers-work/
3. https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers.setTimeout

standard
  1. luckyGirl - 2014 年 10 月 20 日 5:19 下午

    不太明白
    setTimeout(normal(),10); //也会输出3,但是不同于第一种情况,这里的3会被立即输出,没有延时
    setTimeout(returnString,10); //没有输出

    回复
    • lovecicy - 2014 年 10 月 21 日 9:30 上午

      setTimeout(normal(),10)是先把执行normal(),再把返回的值传给setTimeout(),在执行normal()的时候当然会输出3啦。

      setTimeout(returnString,10)则是在10ms后执行returnString(),只是返回console.log(),而不会执行这段字符串

      回复
  2. 疯狂的小鸡 - 2015 年 1 月 12 日 12:26 下午

    楼主的文章写得深入,赞啊,从事了两年的软件研发才知道JS有这样的机制,原来setInterval有这样的限制。
    用度娘搜索setTimeout或者setInterval,得到的结果都是说在指定的时候执行相关函数,坑啊。

    对了,读完后,发觉有几个字好像写错了,虽然无伤大雅,但还是说一下:
    浏览器是这么考虑的:假如有见识特费时,要干一小时,而我们的setInterval告诉它,没个一分钟处理一次我。
    ”假如有见识“ –> ”假如有件事“
    ”没个一分钟“ –> ”每隔一分钟“

    回复
    • lovecicy - 2015 年 1 月 12 日 4:24 下午

      呵呵,谢谢指正,已经改了

      回复

Have your say