单线程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. 【贪心】【堆】Gym -100956D - Greedy Game

    题意:给定n个物品,每个物品对于A和B来说具有不同的价值,记为ai,bi,两人交替取,A先手,A总是贪心地取当前剩下的物品中,对于他价值最高的,如果有多个,则任取一个.问B在最坏情况下,能取到的物品的 ...

  2. [HNOI2004]Language L语言

    2777: [HNOI2004]Language L语言 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 10  Solved: 5[Submit][S ...

  3. React Native 之 搭建开发环境

    现在只能在MAC系统上安装. 1. 首先,HomeBrew 这是一个包管理库,用来下载其它工具. 打开网站:brew.sh 网页有个命令 /usr/bin/ruby -e "$(curl - ...

  4. LINUX 无法登入系统(2017-1-16)

    很好的博文:http://blog.csdn.net/caizi001/article/details/38659189

  5. php短数组写法

    在php 5.4+的版本中,可以使用  []  来表示  array() 例子: // On PHP 5.1 $data = array("foo", "bar" ...

  6. Java经典案例之-判断质数(素数)

    /** * 描述:任意输入两个数n,m(n<m)判断n-m之间有多少个素数,并输出所有素数. * 分析:素数即质数,除1和本身之外,不能被其他自然数整除的数. * 判断素数的方法为:用一个数分别 ...

  7. jQuery原型技术分解

    jQuery原型技术分解 起源----原型继承 用户过javascript的都会明白,在javascript脚本中到处都是 函数,函数可以归置代码段,把相对独立的功能封闭在一个函数包中.函数也可以实现 ...

  8. Nancy简单实战之NancyMusicStore(六):写在最后

    前言 由于公司搬家后,住的地方离上班的地方远了N倍,以前是走路十多分钟就可以到公司的,上班时间也从9:00提早到8:30 现在每天上班都是先坐公交,然后再坐地铁,在这段路上比较浪费时间而且每天都是要6 ...

  9. 初识Dapper

    16年年底开发一个项目,拍卖的项目,对于我这个不入流的程序员来说,雪微是个挑战.程序猿这个行业就是学到老用到老吧.个人比较喜欢sql原生的写法,对EF 还是不怎么感冒,EF 虽然强大,但是用起来还不怎 ...

  10. Dubbo源码学习--集群负载均衡算法的实现

    相关文章: Dubbo源码学习文章目录 前言 Dubbo 的定位是分布式服务框架,为了避免单点压力过大,服务的提供者通常部署多台,如何从服务提供者集群中选取一个进行调用, 就依赖Dubbo的负载均衡策 ...