JavaScript提供定时执行代码的功能,叫做定时器(timer),主要由setTimeout()和setInterval()这两个函数来完成。

setTimeout()

setTimeout函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。

var timerId = setTimeout(func|code, delay)

上面代码中,setTimeout函数接受两个参数,第一个参数func|code是将要推迟执行的函数名或者一段代码,第二个参数delay是推迟执行的毫秒数。

  1.  
    console.log(1);
  2.  
    setTimeout('console.log(2)',1000);
  3.  
    console.log(3);

上面代码的输出结果就是1,3,2,因为setTimeout指定第二行语句推迟1000毫秒再执行。

需要注意的是,推迟执行的代码必须以字符串的形式,放入setTimeout,因为引擎内部使用eval函数,将字符串转为代码。如果推迟执行的是函数,则可以直接将函数名,放入setTimeout。一方面eval函数有安全顾虑,另一方面为了便于JavaScript引擎优化代码,setTimeout方法一般总是采用函数名的形式,就像下面这样。

  1.  
    function f(){
  2.  
    console.log(2);
  3.  
    }
  4.  
     
  5.  
    setTimeout(f,1000);
  6.  
     
  7.  
    // 或者
  8.  
     
  9.  
    setTimeout(function (){console.log(2)},1000);

除了前两个参数,setTimeout还允许添加更多的参数。它们将被传入推迟执行的函数。

  1.  
    setTimeout(function(a,b){
  2.  
    console.log(a+b);
  3.  
    },1000,1,1);

上面代码中,setTimeout共有4个参数。最后那两个参数,将在1000毫秒之后回调函数执行时,作为回调函数的参数。

IE 9.0以下版本,只允许setTimeout有两个参数,不支持更多的参数。这时有三种解决方法,第一种是自定义setTimeout,使用apply方法将参数输入回调函数;第二种是在一个匿名函数里面,让回调函数带参数运行,再把匿名函数输入setTimeout;第三种使用bind方法,把多余的参数绑定在回调函数上面,生成一个新的函数输入setTimeout。

除了参数问题,setTimeout还有一个需要注意的地方:被setTimeout推迟执行的回调函数是在全局环境执行,这有可能不同于函数定义时的上下文环境。

  1.  
    var x = 1;
  2.  
     
  3.  
    var o = {
  4.  
    x: 2,
  5.  
    y: function(){
  6.  
    console.log(this.x);
  7.  
    }
  8.  
    };
  9.  
     
  10.  
    setTimeout(o.y,1000);
  11.  
    // 1

上面代码输出的是1,而不是2,这表示回调函数的运行环境已经变成了全局环境。

再看一个不容易发现错误的例子。

  1.  
    function User(login) {
  2.  
    this.login = login;
  3.  
    this.sayHi = function() {
  4.  
    console.log(this.login);
  5.  
    }
  6.  
    }
  7.  
     
  8.  
    var user = new User('John');
  9.  
     
  10.  
    setTimeout(user.sayHi, 1000);

上面代码只会显示undefined,因为等到user.sayHi执行时,它是在全局对象中执行,所以this.login取不到值。

为了防止出现这个问题,一种解决方法是将user.sayHi放在函数中执行。

  1.  
    setTimeout(function() {
  2.  
    user.sayHi();
  3.  
    }, 1000);

上面代码中,user.sayHi是在函数作用域内执行,而不是在全局作用域内执行,所以能够显示正确的值。

另一种更通用的解决方法,则是采用闭包,将this与当前运行环境绑定。

  1.  
    document.getElementById('click-ok').onclick = function() {
  2.  
    var self = this;
  3.  
    setTimeout(function() {
  4.  
    self.value='OK';
  5.  
    }, 100);
  6.  
    }

上面代码中,setTimeout指定的函数中的this,总是指向定义时所在的DOM节点。

setInterval()

setInterval函数的用法与setTimeout完全一致,区别仅仅在于setInterval指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。

  1.  
    <input type="button" onclick="clearInterval(timer)" value="stop">
  2.  
     
  3.  
    <script>
  4.  
    var i = 1
  5.  
    var timer = setInterval(function() {
  6.  
    console.log(2);
  7.  
    }, 1000);
  8.  
    </script>

上面代码表示每隔1000毫秒就输出一个2,直到用户点击了停止按钮。

与setTimeout一样,除了前两个参数,setInterval 方法还可以接受更多的参数,它们会传入回调函数,下面是一个例子。

  1.  
    function f(){
  2.  
    for (var i=0;i<arguments.length;i++){
  3.  
    console.log(arguments[i]);
  4.  
    }
  5.  
    }
  6.  
     
  7.  
    setInterval(f, 1000, "Hello World");
  8.  
    // Hello World
  9.  
    // Hello World
  10.  
    // Hello World
  11.  
    // ...

如果网页不在浏览器的当前窗口(或tab),许多浏览器限制setInteral指定的反复运行的任务最多每秒执行一次。

setInterval指定的是,“开始执行”之间的间隔,因此实际上,两次执行之间的间隔会小于setInterval指定的时间。假定setInterval指定,每100毫秒执行一次,每次执行需要5毫秒,那么第一次执行结束后95毫秒,第二次执行就会开始。如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始。

  1.  
    var i = 1;
  2.  
    var timer = setInterval(function() {
  3.  
    alert(i++);
  4.  
    }, 2000);

上面代码每隔2000毫秒,就跳出一个alert对话框。如果用户一直不点击“确定”,整个浏览器就处于“堵塞”状态,后面的执行就一直无法触发,将会累积起来。举例来说,第一次跳出alert对话框后,用户过了6000毫秒才点击“确定”,那么第二次、第三次、第四次执行将累积起来,它们之间不会再有等待间隔。

为了确保两次执行之间有固定的间隔,可以不用setInterval,而是每次执行结束后,使用setTimeout指定下一次执行的具体时间。上面代码用setTimeout,可以改写如下。

  1.  
    var i = 1;
  2.  
    var timer = setTimeout(function() {
  3.  
    alert(i++);
  4.  
    timer = setTimeout(arguments.callee, 2000);
  5.  
    }, 2000);

上面代码可以确保两次执行的间隔是2000毫秒。

根据这种思路,可以自己部署一个函数,实现间隔时间确定的setInterval的效果。

  1.  
    function interval(func, wait){
  2.  
    var interv = function(w){
  3.  
    return function(){
  4.  
    setTimeout(interv, w);
  5.  
    func.call(null);
  6.  
    }
  7.  
    }(wait);
  8.  
     
  9.  
    setTimeout(interv, wait);
  10.  
    }
  11.  
     
  12.  
    interval(function(){
  13.  
    console.log(2);
  14.  
    },1000);

上面代码部署了一个interval函数,用循环调用setTimeout模拟了setInterval。

clearTimeout(),clearInterval()

setTimeout和setInterval函数,都返回一个表示计数器编号的整数值,将该整数传入clearTimeout和clearInterval函数,就可以取消对应的定时器。

  1.  
    var id1 = setTimeout(f,1000);
  2.  
    var id2 = setInterval(f,1000);
  3.  
     
  4.  
    clearTimeout(id1);
  5.  
    clearInterval(id2);

下面是一个clearTimeout实际应用的例子。有些网站会实时将用户在文本框的输入,通过Ajax方法传回服务器,用jQuery表示就是下面的写法。

$('textarea').on('keydown', ajaxAction);

这样写有一个很大的缺点,就是如果用户连续击键,就会连续触发keydown事件,造成大量的Ajax通信。这是不必要的,而且很可能会发生性能问题。正确的做法应该是,设置一个门槛值,表示两次Ajax通信的最小间隔时间。如果在设定的时间内,发生新的keydown事件,则不触发Ajax通信,并且重新开始计时。如果过了指定时间,没有发生新的keydown事件,将进行Ajax通信将数据发送出去。

这种做法叫做debounce(防抖动)方法,用来返回一个新函数。只有当两次触发之间的时间间隔大于事先设定的值,这个新函数才会运行实际的任务。假定两次Ajax通信的间隔不小于2500毫秒,上面的代码可以改写成下面这样。

$('textarea').on('keydown', debounce(ajaxAction, 2500))

利用setTimeout和clearTimeout,可以实现debounce方法。

  1.  
    function debounce(fn, delay){
  2.  
    var timer = null; // 声明计时器
  3.  
    return function(){
  4.  
    var context = this, args = arguments;
  5.  
    clearTimeout(timer);
  6.  
    timer = setTimeout(function(){
  7.  
    fn.apply(context, args);
  8.  
    }, delay);
  9.  
    };
  10.  
    }

现实中,最好不要设置太多的setTimeout和setInterval,它们耗费CPU。比较理想的做法是,将要推迟执行的代码都放在一个函数里,然后只对这个函数使用setTimeout或setInterval。

运行机制

setTimeout和setInterval的运行机制是,将指定的代码移出本次执行,等到下一轮Event Loop时,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就等到再下一轮Event Loop时重新判断。这意味着,setTimeout指定的代码,必须等到本次执行的所有代码都执行完,才会执行。

每一轮Event Loop时,都会将“任务队列”中需要执行的任务,一次执行完。setTimeout和setInterval都是把任务添加到“任务队列”的尾部。因此,它们实际上要等到当前脚本的所有同步任务执行完,然后再等到本次Event Loop的“任务队列”的所有任务执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeout和setInterval指定的任务,一定会按照预定时间执行。

  1.  
    setTimeout(someTask,100);
  2.  
    veryLongTask();

上面代码的setTimeout,指定100毫秒以后运行一个任务。但是,如果后面立即运行的任务(当前脚本的同步任务))非常耗时,过了100毫秒还无法结束,那么被推迟运行的someTask就只有等着,等到前面的veryLongTask运行结束,才轮到它执行。

这一点对于setInterval影响尤其大。

  1.  
    setInterval(function(){
  2.  
    console.log(2);
  3.  
    },1000);
  4.  
     
  5.  
    (function (){
  6.  
    sleeping(3000);
  7.  
    })();

上面的第一行语句要求每隔1000毫秒,就输出一个2。但是,第二行语句需要3000毫秒才能完成,请问会发生什么结果?

结果就是等到第二行语句运行完成以后,立刻连续输出三个2,然后开始每隔1000毫秒,输出一个2。也就是说,setIntervel具有累积效应,如果某个操作特别耗时,超过了setInterval的时间间隔,排在后面的操作会被累积起来,然后在很短的时间内连续触发,这可能或造成性能问题(比如集中发出Ajax请求)。

为了进一步理解JavaScript的单线程模型,请看下面这段伪代码。

  1.  
    function init(){
  2.  
    { 耗时5ms的某个操作 }
  3.  
    触发mouseClickEvent事件
  4.  
    { 耗时5ms的某个操作 }
  5.  
    setInterval(timerTask,10);
  6.  
    { 耗时5ms的某个操作 }
  7.  
    }
  8.  
     
  9.  
    function handleMouseClick(){
  10.  
    耗时8ms的某个操作
  11.  
    }
  12.  
     
  13.  
    function timerTask(){
  14.  
    耗时2ms的某个操作
  15.  
    }

请问调用init函数后,这段代码的运行顺序是怎样的?

  • 0-15ms:运行init函数。

  • 15-23ms:运行handleMouseClick函数。请注意,这个函数是在5ms时触发的,应该在那个时候就立即运行,但是由于单线程的关系,必须等到init函数完成之后再运行。

  • 23-25ms:运行timerTask函数。这个函数是在10ms时触发的,规定每10ms运行一次,即在20ms、30ms、40ms等时候运行。由于20ms时,JavaScript线程还有任务在运行,因此必须延迟到前面任务完成时再运行。

  • 30-32ms:运行timerTask函数。

  • 40-42ms:运行timerTask函数。

setTimeout(f,0)

含义

setTimeout的作用是将代码推迟到指定时间执行,如果指定时间为0,即setTimeout(f,0),那么会立刻执行吗?

答案是不会。因为上一段说过,必须要等到当前脚本的同步任务和“任务队列”中已有的事件,全部处理完以后,才会执行setTimeout指定的任务。也就是说,setTimeout的真正作用是,在“任务队列”的现有事件的后面再添加一个事件,规定在指定时间执行某段代码。setTimeout添加的事件,会在下一次Event Loop执行。

setTimeout(f,0)将第二个参数设为0,作用是让f在现有的任务(脚本的同步任务和“任务队列”中已有的事件)一结束就立刻执行。也就是说,setTimeout(f,0)的作用是,尽可能早地执行指定的任务。

  1.  
    setTimeout(function (){
  2.  
    console.log("你好!");
  3.  
    }, 0);

上面代码的含义是,尽可能早地显示“你好!”。

setTimeout(f,0)指定的任务,最早也要到下一次Event Loop才会执行。请看下面的例子。

  1.  
    setTimeout(function() {
  2.  
    console.log("Timeout");
  3.  
    }, 0);
  4.  
     
  5.  
    function a(x) {
  6.  
    console.log("a() 开始运行");
  7.  
    b(x);
  8.  
    console.log("a() 结束运行");
  9.  
    }
  10.  
     
  11.  
    function b(y) {
  12.  
    console.log("b() 开始运行");
  13.  
    console.log("传入的值为" + y);
  14.  
    console.log("b() 结束运行");
  15.  
    }
  16.  
     
  17.  
    console.log("当前任务开始");
  18.  
    a(42);
  19.  
    console.log("当前任务结束");
  20.  
     
  21.  
    // 当前任务开始
  22.  
    // a() 开始运行
  23.  
    // b() 开始运行
  24.  
    // 传入的值为42
  25.  
    // b() 结束运行
  26.  
    // a() 结束运行
  27.  
    // 当前任务结束
  28.  
    // Timeout

上面代码说明,setTimeout(f,0)必须要等到当前脚本的所有同步任务结束后才会执行。

0毫秒实际上达不到的。根据HTML 5标准,setTimeOut推迟执行的时间,最少是4毫秒。如果小于这个值,会被自动增加到4。另一方面,浏览器内部使用32位带符号的整数,来储存推迟执行的时间。这意味着setTimeout最多只能推迟执行2147483647毫秒(24.8天),超过这个时间会发生溢出,导致回调函数将在当前任务队列结束后立即执行,即等同于setTimeout(f,0)的效果

应用

setTimeout(f,0)有几个非常重要的用途。它的一大应用是,可以调整事件的发生顺序。比如,网页开发中,某个事件先发生在子元素,然后冒泡到父元素,即子元素的事件回调函数,会早于父元素的事件回调函数触发。如果,我们先让父元素的事件回调函数先发生,就要用到setTimeout(f, 0)。

  1.  
    var input = document.getElementsByTagName('input[type=button]')[0];
  2.  
     
  3.  
    input.onclick = function A() {
  4.  
    setTimeout(function B() {
  5.  
    input.value +=' input';
  6.  
    }, 0)
  7.  
    };
  8.  
     
  9.  
    document.body.onclick = function C() {
  10.  
    input.value += ' body'
  11.  
    };

上面代码在点击按钮后,先触发回调函数A,然后触发函数C。在函数A中,setTimeout将函数B推迟到下一轮Loop执行,这样就起到了,先触发父元素的回调函数C的目的了。

用户自定义的回调函数,通常在浏览器的默认动作之前触发。比如,用户在输入框输入文本,keypress事件会在浏览器接收文本之前触发。因此,下面的回调函数是达不到目的的。

  1.  
    document.getElementById('input-box').onkeypress = function(event) {
  2.  
    this.value = this.value.toUpperCase();
  3.  
    }

上面代码想在用户输入文本后,立即将字符转为大写。但是实际上,它只能将上一个字符转为大写,因为浏览器此时还没接收到文本,所以this.value取不到最新输入的那个字符。只有用setTimeout改写,上面的代码才能发挥作用。

  1.  
    document.getElementById('my-ok').onkeypress = function() {
  2.  
    var self = this;
  3.  
    setTimeout(function() {
  4.  
    self.value = self.value.toUpperCase();
  5.  
    }, 0);
  6.  
    }

上面代码将代码放入setTimeout之中,就能使得它在浏览器接收到文本之后触发。

由于setTimeout(f,0)实际上意味着,将任务放到浏览器最早可得的空闲时段执行,所以那些计算量大、耗时长的任务,常常会被放到几个小部分,分别放到setTimeout(f,0)里面执行。

  1.  
    var div = document.getElementsByTagName('div')[0];
  2.  
     
  3.  
    // 写法一
  4.  
    for(var i=0xA00000;i<0xFFFFFF;i++) {
  5.  
    div.style.backgroundColor = '#'+i.toString(16);
  6.  
    }
  7.  
     
  8.  
    // 写法二
  9.  
    var timer;
  10.  
    var i=0x100000;
  11.  
     
  12.  
    function func() {
  13.  
    timer = setTimeout(func, 0);
  14.  
    div.style.backgroundColor = '#'+i.toString(16);
  15.  
    if (i++ == 0xFFFFFF) clearInterval(timer);
  16.  
    }
  17.  
     
  18.  
    timer = setTimeout(func, 0);

上面代码有两种写法,都是改变一个网页元素的背景色。写法一会造成浏览器“堵塞”,而写法二就能就不会,这就是setTimeout(f,0)的好处。

另一个使用这种技巧的例子是,代码高亮的处理。如果代码块很大,就会分成一个个小块,写成诸如setTimeout(highlightNext, 50)的样子,进行分块处理。

参考链接

参考来自《JavaScript 标准参考教程(alpha)》,by 阮一峰

有关定时器setTimeout()、setInterval()详解的更多相关文章

  1. setTimeout setInterval 详解

      http://www.jb51.net/article/74606.htm   var tttt=setTimeout('northsnow()',1000);//执行一次 clearTimeou ...

  2. Spring定时器corn表达式详解

    1.结构 Spring定时器corn表达式详解 2.各字段的含义 字段 允许值 允许的特殊字符 秒 0~59 - * / 分 0~59 - * / 小时 0~23 - * / 日期 1~31 - * ...

  3. 单机架设传奇服务器第47课:定时器OnTimer功能详解

    传奇定时器OnTimer功能详解(泡点.时间触发.任务活动) 定时器功能,是服务端中非常常见的一种功能,常见如:泡点脚本.赌博脚本,任务活动指定时间刷怪,时间触发一些都需要用到OnTimer功能,定时 ...

  4. 传奇定时器OnTimer功能详解(泡点、时间触发、任务活动)

    传奇定时器OnTimer功能详解(泡点.时间触发.任务活动) 定时器功能,是传奇服务端中非常常见的一种功能,常见如:泡点脚本.赌博脚本,任务活动指定时间刷怪,时间触发一些都需要用到OnTimer功能, ...

  5. setTimeOut和setInterval详解

    setTimeout和setInterval的语法相同.它们都有两个参数,一个是将要执行的代码字符串,还有一个是以毫秒为单位的时间间隔,当过了那个时间段之后就将执行那段代码.不过这两个函数还是有区别的 ...

  6. Jmeter(十三) - 从入门到精通 - JMeter定时器 - 上篇(详解教程)

    1.简介 用户实际操作时,并非是连续点击,而是存在很多停顿的情况,例如:用户需要时间阅读文字内容.填表.或者查找正确的链接等.为了模拟用户实际情况,在性能测试中我们需要考虑思考时间.若不认真考虑思考时 ...

  7. Jmeter(十四) - 从入门到精通 - JMeter定时器 - 下篇(详解教程)

    1.简介 用户实际操作时,并非是连续点击,而是存在很多停顿的情况,例如:用户需要时间阅读文字内容.填表.或者查找正确的链接等.为了模拟用户实际情况,在性能测试中我们需要考虑思考时间.若不认真考虑思考时 ...

  8. Javascript异步编程之setTimeout与setInterval详解分析(一)

    Javascript异步编程之setTimeout与setInterval 在谈到异步编程时,本人最主要会从以下三个方面来总结异步编程(注意:特别解释:是总结,本人也是菜鸟,所以总结不好的,请各位大牛 ...

  9. 定时器(setTimeout/setInterval)调用带参函数失效解决方法

    也许你曾碰到过这样的问题,不管是setInterval()还是setTimeout(),当code参数里放一个带参函数时,定时器都会失效,看下面这个例子: function test(str){ al ...

随机推荐

  1. poj 3525 求凸包的最大内切圆

    Most Distant Point from the Sea Time Limit: 5000MS   Memory Limit: 65536K Total Submissions: 3640   ...

  2. mongodb的入门学习

    mongodb的入门学习 简介: MongoDB 是一个基于分布式文件存储的数据库.由 C++ 语言编写.旨在为 WEB 应用提供可扩展的高性能数据存储解决方案. MongoDB 是一个介于关系数据库 ...

  3. qu de hanzi 首字母

    Function hztopy(hzpy As String) As StringDim hzstring As String, pystring As StringDim hzpysum As In ...

  4. mysql workbench 导出建表语句

    导出建表语句和数据 server—–>data export—–>选中表  注意,export to dump project folder,这种方式是每个表对应一个sql文件.  导出建 ...

  5. 多个ajax执行混乱问题

    多个ajax执行混乱问题,之前拿ajax取代iframe做响应布局(左侧点击,右侧展示),当执行多个点击事件时会造成一个页面的初始化触发另一个页面的on click的function, 将ajax调为 ...

  6. git(三):第一次github了解使用

    第一次使用github,看了一下使用说明,创建第一个repository,以后还要多学习. 一.Github创建一个新仓库 ······Creat a new repository 创建一个新仓库,点 ...

  7. BZOJ——1607: [Usaco2008 Dec]Patting Heads 轻拍牛头

    http://www.lydsy.com/JudgeOnline/problem.php?id=1607 Time Limit: 3 Sec  Memory Limit: 64 MBSubmit: 2 ...

  8. codevs——1842 递归第一次

    1842 递归第一次  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 白银 Silver 题解       题目描述 Description 同学们在做题时常遇到这种函数 f( ...

  9. axis2调用WSDL接口

    public static JSONObject sendWsdl(String url,String xmlStr){ JSONObject res=new JSONObject(); try { ...

  10. 【flyway】开源的数据库版本管理工具【migration】

    开源的数据库版本管理工具[migration] 记录