JavaScript定时器及相关面试题
在单线程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的相关文章中回答。
参考连接
JavaScript定时器及相关面试题的更多相关文章
- Javascript定时器(三)——setTimeout(func, 0)
setTimeout(func, 0)可以使用在很多地方,拆分循环.模拟事件捕获.页面渲染等 一.setTimeout中的delay参数为0,并不是指马上执行 <script type=&quo ...
- Javascript定时器(二)——setTimeout与setInterval
一.解释说明 1.概述 setTimeout:在指定的延迟时间之后调用一个函数或者执行一个代码片段 setInterval:周期性地调用一个函数(function)或者执行一段代码. 2.语法 set ...
- JavaScript定时器详解
假设有以下场景 setTimeout(function timeoutHandler(){ /*Some timeout handle code that runs for 6ms*/ }, 10); ...
- Javascript 定时器调用传递参数的方法
文章来源: https://m.jb51.net/article/20880.htm 备注:先记下,以后整理: Javascript 定时器调用传递参数的方法,需要的朋友可以参考下. 无论是wind ...
- C# 相关面试试题简单的总结
最近一个搞NET开发的朋友离职了,想让我给他找点关于NET的相关面试题,准备抱一下佛脚,迎接新的挑战. 我赶紧找到以前检索的各种宝典,试题,今天梳理一下关于NET的基础知识点. 1.面向对象语言的三大 ...
- JavaScript定时器的工作原理(翻译)
JavaScript定时器的工作原理(翻译) 标签(空格分隔): JavaScript定时器 最近在看ajax原理的时候,看到了一篇国外的文章,讲解了JavaScript定时器的工作原理,帮助我很好的 ...
- JavaScript定时器作业
JavaScript定时器作业 <!DOCTYPE html> <html lang="zh-CN"> <head> <meta char ...
- 关于JavaScript定时器我的一些小理解
因为自己在平时工作中,有些功能需要用到定时器,但是定时器并不像我们表边上看到的那样,所以这周末我看看书查查资料,深入研究了一下JavaScript中的定时器,那么废话不多说,下面进入我们今天的正题. ...
- Javascript 定时器的使用陷阱 (setInterval)
setTimeout(function(){ // 其他代码 setTimeout(arguments.callee, interval); }, interval); setInterval会产生回 ...
随机推荐
- C++中的IO流
一,标准输入流 1.基本功能(头文件为iostream) char ch = cin.get();// 一次读取一个字符,如果遇到EOF则结束. cin.getline(buf,length);// ...
- linux 同步机制之complete【转】
转自: http://blog.csdn.net/wealoong/article/details/8490654 在Linux内核中,completion是一种简单的同步机制,标志"thi ...
- 3. 托管对象模型的迁移(Core Data 应用程序实践指南)
本章介绍如何添加模型版本及模型映射,演示几种迁移技术,供在升级模型时使用. 3.1. 修改托管对象模型 3.2. 添加模型版本 3.3. 轻量级迁移方式 3.4. 默认的迁移方式 3.5. 通过迁移管 ...
- 判断移动端设备: navigator.userAgent.toLowerCase()
判断你的浏览设备: navigator.userAgent.toLowerCase(); (返回当前用户所使用的是什么浏览器,将获得的信息变成小写) function browserRedirect( ...
- PHP函数 mysql_real_escape_string 与 addslashes 的区别
addslashes 和 mysql_real_escape_string 都是为了使数据安全的插入到数据库中而进行的过滤,那么这两个函数到底是有什么区别呢? 首先,我们还是从PHP手册入手: 手册上 ...
- jQuery 鼠标滚轮插件 jquery.mousewheel.js
jQuery Mousewheel Plugin,用于添加跨浏览器的鼠标滚轮支持.mousewheel事件的处理函数有一点小小的变化,它除了第一个参数event 外,还接收到第二个参数delta.通过 ...
- Java Swing paint repaint update 方法的关系
Java Swing paint repaint update 方法的关系: 参考:http://blog.csdn.net/xiaoliangmeiny/article/details/691665 ...
- SSH系统介绍
SSH的系统中,对象的调用流程是:JSP->Action->Service->DAO->Hibernate,数据的流向是ActionFormBean接受用户的数据,Action ...
- Qt Quick编程(1)——QML的核心部分ECMAScript
说道QML,不得不先说一下ECMAScript: ECMAScript语言的标准是由Netscape.Sun.微软.Borland等公司基于JavaScript和JScript锤炼.定义出来的. EC ...
- CodeForces757A
A. Gotta Catch Em' All! time limit per test 1 second memory limit per test 256 megabytes input stand ...