在JavaScript中,有两种创建定时器的方法,setTimeout和setInterval,两者都可接受两个参数,第一个参数可以是JavaScript字符串或是函数,第二个参数是以毫秒为单位的数值,以setTimeout为例:
setTimeout(function () {
console.log(1)
},1000)
给定时器传入JavaScript字符串的方式有着造成性能底下的问题,因为JavaScript字符串参数需要重新启动一个解析器来进行解析,启动解析器有着不少的性能开销,并且解析的过程会进入安全模式,这种模式下不会有性能优化处理,因此,不论是定时器、Function构造函数、或者eval,都应避免传入JavaScript字符串这种方式。
执行上面的定时器,一秒钟后,数字1会在控制台中被打印出来。所以,我们理所应当地认为第二个参数代表的就是回调函数被执行的延迟时间。但事实上,这样的认识并非准确,这个时间参数代表的实际是代码被添加进任务队列的时间。
JavaScript是单线程的,同一时间只能做一件事,所有的任务都需要排队等待执行,任务有异步与同步之分,如setTimeout,setInterval,promise都会生成异步任务,异步任务的特性是任务的执行是在将来的某个时间点,反之同步任务的特性则是立马执行,所以,异步任务的执行永远是在同步任务之后,这是因为JavaScript事件循环机制会将所有的异步任务推入相应的异步任务队列当中,当所有的同步任务执行完毕之后,才会开始执行异步任务队列当中的任务。
综上所述,我们需明白,定时器的第二个参数代表的是异步任务被推入异步任务队列的时间点,然后等待执行,而不是任务被执行的时间,下面,用一个例子证明:
setTimeout(function() {
console.log('我执行了')
},300)
for (var i = 0; i < 10000;i++) {
console.log(i);
}
上述代码中,需要等待比300毫秒更长的一段时间后才会在控制台输出’我执行了’,这是因为for循环执行时间过长的缘故,这是一个同步的循环任务,JavaScript会在执行完所有的同步任务后,才会执行异步任务,在这样的机制中,如果前面的任务执行时间过长,就会导致后面的任务只能干巴巴等待,所以,300毫秒后,异步任务被推入异步任务队列当中,但由于此时for循环仍在运行中,异步任务并不能立刻得到执行。
setInterval为循环定时器,不同于setTimeout,循环定时器会在指定的时间间隔中,反复将任务插入异步任务队列。
setInterval(function () {
console.log(1)
},1000)
运行上例,每隔一秒便会在控制台输出1。这很简单,比较复杂的是,JavaScript进程发生阻塞的时候,
setInterval(function () {
console.log(new Date().getTime())
},1000)
var stop = true;
var start = new Date().getTime();
while (stop) {
if (new Date().getTime() > (start + 2500)) {
stop = false;
}
}
上例中,while循环的运行时间为2500毫秒,这导致循环定时器按时插入的异步任务必须等到2500毫秒后才可执行,当异步任务队列中的任务还未完成的时候,循环定时器不会再向队列中添加任务。你会在控制台看到如下景象:
2500毫秒后输出当前时间戳,
500毫秒后输出当前时间戳,
1000毫秒后输出当前时间戳,
1000毫秒后输出当前时间戳,
1000毫秒后输出当前时间戳,
重复上一步
以下为您讲解具体流程:
1、循环定时器被交给其他模块进行处理,负责处理的模块将在1000毫秒后将异步任务插入任务队列当中。
2、while循环开始运行,当运行时间到1000毫秒的时候,异步任务被插入任务队列当中,等待执行。
3、while循环进行到2000毫秒,虽到了指定的时间,但由于任务队列中存在未完成的任务,所以这次的任务不会被添加进队列。
4、while循环在进行到2500毫秒的时候结束,JavaScript进程处于空闲状态,随即读取异步任务队列中的任务予以执行,这时候,在控制台中输出了当前的时间戳1548563753501。
5、3000毫秒的时候(从while循环进行到2000毫秒处算起,即一秒钟后),异步任务被添加到任务队列当中并立即得到执行,在控制台中输出当前的时间戳1548563753993,
6、每隔一秒后输出当前时间戳。
通过上述流程讲解,我们发现了循环定时器的问题:
1、由于进程阻塞,导致后续的任务由于前一个任务未完成而被跳过
2、由于进程阻塞,导致迟迟未能完成的任务与后续的任务之间的执行间隔比指定的时间要短。
需要注意的是,这是遵循W3C标准实现的循环定时器的情况下会遇到的问题,不同的浏览器,不同的版本对循环定时器的实现可能不同于标准,由此产生的行为与标准会有出入,因此,我们推荐使用下面的方法,来实现循环定时器的功能:
function interval (func,time) {
var timer = setTimeout(function() {
func();
setTimeout(arguments.callee,time);
},time);
return timer;
}
interval(function(){
console.log(1);
},1000);
这是使用单次定时器进行自调用完成的,这样一来,当任务被阻塞的时候,就不会开启下一个定时器,保证了执行任务之间的间隔与忽略的问题。
数组分块技术
在某些情况下,我们可能需要对庞大的数据进行处理、执行一些复杂耗时的操作。对此,我们应当注意两点:
1、JavaScript的运行将会阻塞所有其他的工作,阻塞时,用户就不能对界面进行操作。
2、JavaScript脚本的运行有时间限制,超出运行时长浏览器会弹出警报。
为了避免JavaScript长时间运行,我们可以将一长段任务分段执行,比如,我们需要处理一个很长的数组:
var data = [1,2,3,4,5,6,7];
function fn (handle,data) {
setTimeout(function () {
var item = data.shift();
handle(item);
if (data.length > 0) {
setTimeout(arguments.callee,100);
}
},100)
}
fn(function(item) {
console.log(item);
},data);
此方法运用了单此定时器来实现,将原本需要一次性长时间执行完毕的操作,分成了一块块由异步任务进行处理,这样,就能够让JavaScript进程有空闲的时间,避免了长时间运行脚本的问题。
函数节流
在某些时候,由于某些操作,会造成大量重复的计算,非常地消耗性能,比如,用户拖动浏览器窗口的右下角变换窗口尺寸的时候,我们需要知道变化后的窗口尺寸,于是我们在onresize事件中获取窗口尺寸信息:
window.onresize = function () {
…
}
结果是,当浏览器窗口发生每一像素的变动时,都会调用事件处理器,由此造成大量重复操作,这时,我们可以使用函数节流的方式:
function throttle (handle) {
clearTimeout(handle.timerId);
handle.timerId = setTimeout(handle,300);
}
function getWindowSize () {
console.log('size');
}
window.onresize = function () {
throttle(getWindowSize);
}
这种方法的核心技巧也用到了单次定时器,函数运行的时候,会创建一个定时器,定时器将会在300毫秒后将任务推入任务列队,在这300毫秒期间,如果函数再次被调用,那么定时器就会被销毁,再重新创建一个定时器,这就保证了函数再被频繁快速调用的时候,最终只执行一次的效果。