在JavaScript中,定时器看似简单,其实挺复杂的,与JavaScript的运行机制。我们都知道JavaScript是单线程的,这里说的单线程指的是JavaScript引擎是单线程的,浏览器是多线程的。JavaScript的运行机制不是本篇文章讨论范围,本文主要讨论setTimeout。定时器很常用,也很容易出现在面试题中,这篇文章将围绕定时器的语法,常见例子,以及一些要注意的地方和容易弄错的地方。
一、setInterval语法
语法setInterval(str,time);setInterval(function,time,parmam1,param2…);第一个参数为字符串或者函数,第二个参数为定时的时间,第三个参数可以作为传给函数的参数(可选)。返回值为一个定时器的ID(数字);一般将这个ID传给clearinterval()清除定时器。setInterval定时器是间歇性调用,即每隔设定的时间调用一次,在不加干涉的情况下,间歇调用将会一直执行到页面卸载。例如
setInterval(function(){
console.log(1);
},2000);
每隔2秒打印一次值,这段如果不使用clearInterval清除定时器则会一直输出1
二、setTimeout语法
setTimeout的语法跟setInterval一样,只不过setTimeout不是间歇调用,而是超时调用,而是在设定时间是执行一次定时器里面的代码,注意是执行一次。清除定时器使用clearTimeout().例如
setTimeout(function(){
console.log(1);
},2000);
这段代码只打印一次1,在编写程序过程中建议多使用setTimeout代替setInterval。
三、执行机制
setInterval与setTimeout是异步执行的,也就是说在同一个循环中,等到其他同步代码执行完后,再执行定时器的代码。如这段代码:
console.log(1);
setTimeout(function(){
console.log(2);
},1000);
console.log(3);
输出的顺序是1,3,2,而不是1,2,3;定时器中设定的时间不一定就是在1秒就打印出2,而是等其他同步代码执行完后,再等待一秒打印。如果前面的代码耗时很长那么可能要很久才执行。那么现在来理解一下
setTimeout(f,0);
当定时为0的时候是立即打印吗?当然不是,就像前面说的,它会等其他同步任务执行完后再执行,那么将时间定为0就表示当其他同步任务执行完后立即执行定时器里面的代码。setTimeout(f,0)的应用:
1. 用来改变事件的执行顺序:例如我们想要在表单中输入英文字母时立即将它转换为小写字母,加如我们使用这段代码:
document.getElementById('in').onkeypress=function(event){
this.value=this.value.toLowerCase();
}
那么它只能在键盘输入下一个字符时才把上一个字符变为小写,但我们需要的是输入字符立即转换。此时可以使用setTimeout(f,0)解决这个问题
document.getElementById('in').onkeypress=function(event){
var _this=this;
setTimeout(function(){
_this.value=_this.value.toLowerCase();
},0);
}
2.由于setTimeout(f,0)实际上意味着,将任务放到浏览器最早可得的空闲时段执行。所以在处理那些计算量大、耗时长的任务,常常会被放到几个小部分(容易造成阻塞),分别放到setTimeout(f,0)里面执行。简单来讲就是任务拆分。
//写法一
var div=document.getElement('div1');
for(var i=0xA00000;i<0xFFFFF;i++){
div.style.backgroundColor='#'+i.toString(16);
}
//写法二
var div=document.getElement('div1');
var timer;
var i=0x100000;
function f(){
timer=setTimeout(f,0);
div.style.backgroundColor='#'+i.toString(16);
if(i++==0xFFFFF) clearTimeout(timer);
}
timer=setTimeout(f,0);
四、setTimeout与闭包
经过前面的分析,现在我们来看一下小例子
for(i=0;i<5;i++){
console.log(i);
}
相信这段代码输出什么大家肯定不陌生吧。那么接下来这段代码呢?
for(i=0;i<5;i++){
setTimeout(function(){
console.log(i);
},1000);
}
没错,这段代码并不会如我们所愿的输出0,1,2,3,4;而是输出连续输出5个5。为什么呢?如果我们在setTimeout后面再加一个console.log(i),通过控制台可以看到,先输出0、1、2、3、4,再输出5个5.
for(i=0;i<5;i++){
setTimeout(function(){
console.log(i);
},1000);
console.log(i);
}//
这就跟我前面说的setTimeout是异步的有关,它会等整个循环同步任务执行完后再执行。那么我们可以通过闭包来解决问题,每循环一次,便立即将i值传入自执行函数中,传给setTimeout。
for(i=0;i<5;i++){
(function(i){
setTimeout(function(){
console.log(i);
},1000);
})(i);
}
为什么这下面这段代码失败了呢?
for(i=0;i<5;i++){
(function(){
setTimeout(function(){
console.log(i);
},1000);
})(i);
}
这是因为没有将每一次循环的i值传给setTimeout.
五、正常任务和微任务
我们来考虑一个问题,当setTimeout和Promise同时出现时,两个都是异步执行的,那么谁先执行呢?考虑下面代码:
console.log(1);
setTimeout(function(){
console.log(2);
},0);
Promise.resolve().then(function(){
console.log(3);
});
通过运行我们可以发现输出的结果是1、3、2;即setTimeout在Promise之后执行。原因是setTimeout是正常任务,Promise是微任务。setTimeout是在下一次Event Loop中执行,而Promise是在本轮Event Loop结束时执行。正常任务有
- setInterval
- setTimeout
- setImmediate
- I/O
- 各种事件(比如鼠标单击事件)的回调函数
六、一些注意事项
还记得上面的这个例子吗
document.getElementById('in').onkeypress=function(event){
var _this=this;
setTimeout(function(){
_this.value=_this.value.toLowerCase();
},0);
}
在这里将当前this保存下来,不然在settimeout运行时this会指向全局对象。现在来详细了解一下setTimeout指向的问题。由setTimeout()调用的代码运行在与所在函数完全分离的执行环境上。这会导致,这些代码中包含的 this 关键字在非严格模式会指向 window (或全局)对象,严格模式下为 undefined。例如这段代码
var a=1;
function obj(){
this.a=2,
this.getA=function(){
console.log(this.a);
}
this.getAlate=function(){
setTimeout(function(){
console.log(this.a);
},1000);
}
}
var o=new obj();
o.getA();//2
o.getAlate();//1 this指向全局window
从输出结果可看出,setTimeout中的this指向了window对象。解决这种问题一般有三种方法
- 将this保存下来,通过闭包访问
var a=1;
function obj(){
var that=this;//保存this
this.a=2,
this.getA=function(){
console.log(this.a);
}
this.getAlate=function(){
setTimeout(function(){
console.log(that.a);
},1000);
}
}
var o=new obj();
o.getA();//2
o.getAlate();//2
- 使用bind函数改变this指向
var a=1;
function obj(){
this.a=2,
this.getA=function(){
console.log(this.a);
}
this.getAlate=function(){
setTimeout(function(){
console.log(this.a);
}.bind(this),1000);//绑定this
}
}
var o=new obj();
o.getA();//2
o.getAlate();//2
- 使用es6的箭头函数
var a=1;
function obj(){
this.a=2,
this.getA=function(){
console.log(this.a);
}
this.getAlate=function(){
setTimeout(()=>{
console.log(this.a);
},1000);
}
}
var o=new obj();
o.getA();//2
o.getAlate();//2
参考文章:
MDN文档