单线程JavaScript这篇文章中,在介绍JavaScript单线程的同时,也介绍了setTimeout是如何工作的。但是对于定时器的一些内容,并没有做深入的讨论。这篇文章,会详细说说JS的两种定时器,setTimeout和setInterval,以及它们的工作方式。同时,会谈谈有关setTimeout的面试题。

setInterval

setInterval,也称为间歇调用定时器,是指允许设置间歇时间来调用定时器代码在特定的时刻执行。也就是说,setInterval会在每隔指定的时间就执行一次代码。

setInterval属于window对象上的私有方法,它可以接收多个参数,

第一个参数可以是一个函数,也可以是一个字符串。

第二个参数是每次执行之前需要等待的毫秒数,这里有一个很大的误区就是,当设定时间之后,很多人认为会立即执行定时器,其实不是。设定一个 150ms 后执行的定时器不代表到了 150ms 代码就立刻执行,它表示代码会在 150ms 后被加入到任务队列中。如果在这个时间点上,主线程上的所有同步任务都执行完毕,并且任务队列上没有其他任务,那么这个任务会被执行;如果主线程上的同步任务未执行完毕,且任务队列上还存在其他异步任务(包括时间更短的定时器),这时候就要等待以上同步任务和异步任务执行完毕之后,这个150ms的任务才会开始执行。

第三个参数以后是指传入函数的一些参数。其中,只有第一个参数是必须的,其他都是可选的。在默认情况下,第二个参数默认值为0。但是0毫秒实际上也是达不到的。根据HTML 5标准,setTimeout推迟执行的时间,最少是5毫秒。如果小于这个值,会被自动增加到5ms。

//let timer = setInterval(func[, delay, param1, param2, ...]);
let timer = setInterval(function(a, b) {
    console.log(a, b);
}, 1000, 1, 2);
//在执行栈为空时,每隔一秒钟就会输出 1, 2

//不建议这样使用!传递字符串会导致性能损失
let timer = setInterval("alert('Hello world')", 1000);

调用完setInterval之后,该方法会返回一个定时器ID,主要用于取消超时调用。

关于setInterval间歇调用定时器,在MDN和《JavaScript高级程序设计(第三版)》上都是不推荐使用的,因为setInterval会带来一些问题。所以,一般情况下,我们会使用setTimeout来代替setInterval。但作为学习,还是要理解其中的原理。

setInterval问题在于(1)某些间隔会被跳过;(2)多个定时器代码之间的间隔可能会比预期的小。

假设,某个 onclick 事件处理程序使用 setInterval() 设置了一个 200ms 间隔的重复定时器。如果事件处理程序花了 300ms 的时间完成,同时定时器代码也花了差不多的时间,就会同时出现跳过间隔且连续运行定时器代码的情况。



这个例子中的第 1 个定时器是在 205ms 处添加到队列中的(即使任务队列为空,0ms实际上是达不到的,因此至少为5ms),但是直到过了 300ms 处才能够执行。当执行这个定时器代码时,在 405ms 处又给任务队列添加了另外一个副本。在下一个间隔,即 605ms 处,第一个定时器代码仍在运行,同时在任务队列中已经有了一个定时器代码的实例。结果是,在这个时间点上的定时器代码不会被添加到队列中。结果在 5ms 处添加的定时器代码结束之后,405ms 处添加的定时器代码就立刻执行。因此,《JavaScript高级程序设计(第三版)》建议,使用超时调用(setTimeout)来模拟间歇调用(setInterval)的是一种最佳模式,原因是后一个间歇调用可能会在前一个间歇调用结束之前启动。

setTimeout

关于setTimeout,它的语法同setInterval。

由于setInterval间歇调用定时器存在一些问题,所以一般会使用setTimeout代替setInterval,至少我本人在开发中是不会使用setInterval的..替换代码如下。

setTimeout(function timer() {
    //需要执行的代码
    //setTimeout会等到定时器代码执行完毕之后才会重新调用自身(递归),要注意的是要给匿名函数添加一个函数名,以便调用自身。
    setTimeout(timer, 1000);
}, 1000)

这样做的好处是,在前一个定时器执行完毕之前,不会向任务队列中插入新的定时器代码,因此确保不会有任何缺失的间隔。而且,它可以保证在下一次定时器代码执行之前,至少要等待指定的间隔,避免了连续执行。这个模式主要用于重复定时器。再看看一些实例。

let num = 0;
let max = 10;

setTimeout(function timer() {
    num++;
    console.log(num);
    if (num === max) {return}
    setTimeout(timer, 500)
}, 500);
//或者是
setTimeout(function timer() {
    num++;
    console.log(num);
    if (num < max) {setTimeout(timer, 500)}
}, 500);

综上,由于setInterval间歇调用定时器会因为在定时器代码未执行完毕时又向任务队列中添加定时器代码,导致某些间隔被跳过等问题,所以应使用setTimeout代替setInterval

有关setTimeout的面试题

关于setTimeout的面试题,主要是循环中使用定时器以及定时器中this的指向性问题。在setTimeout内部,this绑定采用默认绑定规则,也就是说,在非严格模式下,this会指向window;而在严格模式下,this指向undefined。详细可参考此答案如何理解JavaScript中的this关键字

对于循环中使用定时器,问题如下,然后各种问题慢慢开拓...

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000 * i)
}
//以上代码输入什么?

回答:以上代码输出5个5,并且每隔1s输出一个,一共用时4s。这里我想解释一下为什么会这样子输出。以下解释为个人想法,仅供参考。

我们给代码做一些调整。

for (var i = 0; i < 5; i++) {
    let timer = setTimeout(function() {}, 1000 * i)
    console.log(timer);
    //输出1, 2, 3, 4, 5
}

控制台输出了5个不同的定时器ID,说明在for循环当中,创建了5个setTimeout定时器。而由于以上问题的代码是在4秒内输出了5个5,并且第一个5立即输出。因此,5个setTimeout定时器是同时创建的。并且,setTimeout的第二个参数(指定多少ms将定时器推入任务队列中),并非引用的是全局作用域的i(即循环结束退出时的),而是正常情况,即按照循环变量i的累加。因此,可以将以上代码改写。

setTimeout(function() {
    console.log(5);
}, 0);
setTimeout(function() {
    console.log(5);
}, 1000);
setTimeout(function() {
    console.log(5);
}, 2000);
setTimeout(function() {
    console.log(5);
}, 3000);
setTimeout(function() {
    console.log(5);
}, 4000);

这里需要注意的是,setTimeout回调函数中的i引用的是全局作用域下的i(即循环结束时的i),而设定时间的i与for循环的变量i累加相同。

如果有不同意见的博友,请给我留言,共同学习。

问题二:问题一的代码如何让其输出0, 1, 2, 3, 4呢?

回答:这里有两种解决方法,不过其中的原理都相同,即给setTimeout定时器外层创建一个块作用域,或者是创建函数作用域以形成闭包。

//方法一:ES6 let关键字,创建块作用域
for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000 * i)
}
//以上代码实际上是这样的
for (var i = 0; i < 5; i++) {
    let j = i;  //闭包的块作用域
    setTimeout(function() {
        console.log(i);
    }, 1000 * i);
}

//方法二:IIFE
for (var i = 0; i < 5; i++) {
    (function iife(j) {     //闭包的函数作用域
        setTimeout(function() {
            console.log(j);
        }, 1000 * i);   //这里将i换为j, 可以证明以上的想法。
    })(i);
}
//实际上,函数参数,就相当于函数内部定义的局部变量,因此下面的写法是相同的。
for (var i = 0; i < 5; i++) {
    (function iife() {
        var j = i;
        setTimeout(function() {
            console.log(j);
        }, 1000 * i);   //如果这里将i换为j, 可以证明以上的想法。
    })();
}

这里简单说明方法二使用立即执行的函数表达式的原因。

给定时器外层创建了一个IIFE,并且传入变量i。此时,setTimeout会形成一个闭包,记住并且可以访问所在的词法作用域。因此,就会正常输出1, 2, 3, 4。

问题三: 如果原问题改为如下,会输出什么?

for (var i = 0; i < 5; i++) {
    setTimeout((function() {
        console.log(i);
    })(), 1000 * i);
}

回答:立即输出1, 2, 3, 4。因为是setTimeout的第一个参数是函数或者字符串,而此时函数又立即执行了。因此,此时的定时器无效了,直接输出1, 2, 3, 4。上面的代码等同于如下

for (var i = 0; i < 5; i++) {
    (function() {
        console.log(i); //0, 1, 2, 3, 4
    })();
}

问题四,代码如下,输出顺序是什么?

console.log(1);
setTimeout(function() {
  console.log(2);
}, 0);
new Promise(function(resolve, reject) {
    console.log(3);
    resolve();
}).then(function() {
    console.log(4);
}).then(function() {
    console.log(5);
})
console.log(6);

回答:此时的输出顺序是1, 3, 6, 4, 5, 2。这里涉及Promise对象,这道题的解释先留着,等到介绍Promise时再在Pormise的相关文章中回答。

参考连接

定时器

window.setTimeout

window.setInterval

单线程JavaScript

如何理解 JavaScript 中的 this 关键字?

深入理解javascript函数参数与闭包(一)

深入理解javascript闭包(二)

什么是闭包?

JavaScript定时器及相关面试题的更多相关文章

  1. Javascript定时器(三)——setTimeout(func, 0)

    setTimeout(func, 0)可以使用在很多地方,拆分循环.模拟事件捕获.页面渲染等 一.setTimeout中的delay参数为0,并不是指马上执行 <script type=&quo ...

  2. Javascript定时器(二)——setTimeout与setInterval

    一.解释说明 1.概述 setTimeout:在指定的延迟时间之后调用一个函数或者执行一个代码片段 setInterval:周期性地调用一个函数(function)或者执行一段代码. 2.语法 set ...

  3. JavaScript定时器详解

    假设有以下场景 setTimeout(function timeoutHandler(){ /*Some timeout handle code that runs for 6ms*/ }, 10); ...

  4. Javascript 定时器调用传递参数的方法

    文章来源:  https://m.jb51.net/article/20880.htm 备注:先记下,以后整理: Javascript 定时器调用传递参数的方法,需要的朋友可以参考下. 无论是wind ...

  5. C# 相关面试试题简单的总结

    最近一个搞NET开发的朋友离职了,想让我给他找点关于NET的相关面试题,准备抱一下佛脚,迎接新的挑战. 我赶紧找到以前检索的各种宝典,试题,今天梳理一下关于NET的基础知识点. 1.面向对象语言的三大 ...

  6. JavaScript定时器的工作原理(翻译)

    JavaScript定时器的工作原理(翻译) 标签(空格分隔): JavaScript定时器 最近在看ajax原理的时候,看到了一篇国外的文章,讲解了JavaScript定时器的工作原理,帮助我很好的 ...

  7. JavaScript定时器作业

    JavaScript定时器作业 <!DOCTYPE html> <html lang="zh-CN"> <head> <meta char ...

  8. 关于JavaScript定时器我的一些小理解

    因为自己在平时工作中,有些功能需要用到定时器,但是定时器并不像我们表边上看到的那样,所以这周末我看看书查查资料,深入研究了一下JavaScript中的定时器,那么废话不多说,下面进入我们今天的正题. ...

  9. Javascript 定时器的使用陷阱 (setInterval)

    setTimeout(function(){ // 其他代码 setTimeout(arguments.callee, interval); }, interval); setInterval会产生回 ...

随机推荐

  1. [Angular Tutorial] 12 -Event Handlers

    在这一步中,您将会在电话细节页面添加一个可点击的电话图片转换器. ·电话细节页面展示了当前电话的一张大图片和几张相对较小的略图.如果我们能仅仅通过点击略图就能把大图片换成略图就好了.让我们看看用Ang ...

  2. Android滚动选择控件

    现在觉得github特别方便,我一般直接使用github中的内容, https://github.com/wangjiegulu/WheelView 这里面都有详细的介绍

  3. 匿名函数里的this的执行环境和指向--javascript

    重新看了下闭包,在javascript高级程序设计第二版里的闭包里有如下例子,例子中介绍说匿名函数的执行环境具有全局性和this指向window,对于这句话很费解,所以就想个方法验证下. var na ...

  4. 初学杂文 String类

    String: 两个字符床  String stra 和String strb stra = "hello " ; strb = "hello " 在对象池中开 ...

  5. Mysql 多列形成主键(复合主键 )

    什么是数据表的复合主键 所谓的复合主键 就是指你表的主键含有一个以上的字段组成 比如 create table test (    name varchar(19),    id number,    ...

  6. Python dir()函数

    您可以使用内置的dir()函数列出一个定义对象的标识符.例如,对于一个模块,包括在模块中定义的函数,类和变量. 当你给dir()提供一个模块名字时,它返回在那个模块中定义的名字的列表.当没有为其提供参 ...

  7. UIscrollView 代理

    // // UIDemoViewController.m // 06-1UIScrollDemo // // Created by k on 14-9-4. // Copyright (c) 2014 ...

  8. Repository 设计模式介绍(转)

    在DDD设计中大家都会使用Repository pattern来获取domain model所需要的数据. 1.什么事Repository? "A Repository mediates b ...

  9. c++中冒号(:)和双冒号(::)的用法(void文章::变乱()、子类:父类)

    1.冒号(:)的用法 (1)表示机构内位域的定义(即该变量占几个bit空间) typedef struct _XXX{ unsigned char a:4; unsigned char c; } ; ...

  10. js与php传递参数

    这个问题在网页开发时经常遇到,其实解决办法非常简单,就是几行代码的事,不过各种js.php书上都没有,百度下来也乱七八糟的,有的能用,有的不能用.小编遇到这问题时认认真真研究了一上午,研究出一点心得, ...