定义
setTimeout()和setInterval()经常被用来处理延时和定时任务。setTimeout() 方法用于在指定的毫秒数后调用函数或计算表达式,而setInterval()则可以在每隔指定的毫秒数循环调用函数或表达式,直到clearInterval把它清除。setTimeout()只执行一次,而setInterval可以多次调用。
基本用法
setTimeout的基本用法
按照官方的介绍就是我们可以通过以下三种方式来使用setTimeout,每次调用setTimeout都会产生一个timerID,这个timeID是为了以后我们可通过使用clearTimeout来进行清除该定时器。
var timeoutID = setTimeout(function[, delay, arg1, arg2, ...]);
var timeoutID = setTimeout(function[, delay]);
var timeoutID = setTimeout(code[, delay]);
参数介绍
-
function:是你想要在到期时间(
delay
毫秒)之后执行的
-
code:这是一个可选语法,你可以使用字符串而不是
function
,在
delay
毫秒之后编译和执行字符串 (使用该语法是**不推荐的,原因和使用
eval()
一样,有安全风险)
-
delay:可选,延迟的毫秒数 (一秒等于1000毫秒),函数的调用会在该延迟之后发生。如果省略该参数,delay取默认值0,意味着“马上”执行,或者尽快执行。不管是哪种情况,实际的延迟时间可能会比期待的(delay毫秒数) 值长
-
arg1, ..., argN
可选,附加参数,一旦定时器到期,它们会作为参数传递给
function
setInterval的基本使用
setInterval的使用其实和setTimeout的很像,都是基本的调用,返回一个timerID,但是这个timeID是通过clearInterval来进行清除的。
var intervalID = setInterval(func, delay, [arg1, arg2, ...]);
var intervalID = setInterval(code, delay);
参数介绍
这个其实也不做过多的介绍,这里面的参数其实是和setTimeout的参数的含义是一样的。
任务执行队列
首先我们都知道JavaScript其实是运行在单线程的环境中的(h5中引入了一个Web Worker,来实现JavaScript的多线程)。并且JavaScript中存在宏任务和微任务这两个概念。而setTimeout和setInterval则是属于宏任务的范畴。因此就会出现时间间隔几乎是相同的,但不精确。例如:
var _d = new Date()
setTimeout(function() {
var _d2 = new Date()
console.log(_d2 - _d)
}, 100)
// 本地的测试的输出时间是230,这个时间可能不一致,
出现了上述的情况,这是为什么呢?
时间间隔几乎是相同的,但不精确,这是为什么呢?
原因在于我们对JavaScript定时器存在一个误解,JavaScript其实是运行在单线程的环境中的,这就意味着定时器仅仅是计划代码在未来的某个时间执行,而具体执行时机是不能保证的,因为页面的生命周期中,不同时间可能有其他代码在控制JavaScript进程。在页面下载完成后代码的运行、事件处理程序、Ajax回调函数都是使用同样的线程,实际上浏览器负责进行排序,指派某段程序在某个时间点运行的优先级。
我们可以可以把JavaScript想象成在时间线上运行。当页面载入的时候首先执行的是页面生命周期后面要用的方法和变量声明和数据处理,在这之后JavaScript进程将等待更多代码执行。当进程空闲的时候,下一段代码会被触发
除了主JavaScript进程外,还需要一个在进程下一次空闲时执行的代码队列。随着页面生命周期推移,代码会按照执行顺序添加入队列,例如当按钮被按下的时候他的事件处理程序会被添加到队列中,并在下一个可能时间内执行。在接到某个Ajax响应时,回调函数的代码会被添加到队列。JavaScript中没有任何代码是立即执行的,但一旦进程空闲则尽快执行。定时器对队列的工作方式是当特定时间过去后将代码插入,这并不意味着它会马上执行,只能表示它尽快执行。
知道了这些后,我们就能明白,如果想要精确的时间控制,是不能依赖于JavaScript的setTimeout函数的。但是如何实现0秒出发的timeout的功能呢,我曾经看到过有人通过使用postMessage来实现的:具体的代码如下:
(function() {
var timeouts = [];
var messageName = "zero-timeout-message";
// Like setTimeout, but only takes a function argument. There's
// no time argument (always zero) and no arguments (you have to
// use a closure).
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, "*");
}
function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}
window.addEventListener("message", handleMessage, true);
// Add the one thing we want added to the window object.
window.setZeroTimeout = setZeroTimeout;
})();
重复的定时器
但这个方式的问题在于定时器的代码可能在代码再次被添加到队列之前还没有执行完成,结果导致循环内的判断条件不准确,代码多执行几次,之间没有停顿。不过JavaScript已经解决这个问题,当使用setInterval()时,仅当没有该定时器的其他代码实例时才将定时器代码插入队列。这样确保了定时器代码加入到队列的最小时间间隔为指定间隔。
这样的规则带来两个问题
-
某些间隔会被跳过
-
多个定时器的代码执行之间的间隔可能比预期要小
为了避免这两个缺点,我们可以使用setTimeout()来实现重复的定时器这样每次函数执行的时候都会创建一个新的定时器,第二个setTimeout()调用使用了agrument.callee 来获取当前实行函数的引用,并设置另外一个新定时器。这样做可以保证在代码执行完成前不会有新的定时器插入,并且下一次定时器代码执行之前至少要间隔指定时间,避免连续运行。
尽量不用setInterval()
-
原因一、setInterval()无视代码错误
setInterval有个讨厌的习惯,即对自己调用的代码是否报错这件事漠不关心。换句话说,如果setInterval执行的代码由于某种原因出了错,它还会持续不断(不管不顾)地调用该代码。
-
原因二、setInterval无视网络延迟
假设你每隔一段时间就通过Ajax轮询一次服务器,看看有没有新数据(注意:如果你真的这么做了,那恐怕你做错了;建议使用“补偿性轮询”(backoff polling))。而由于某些原因(服务器过载、临时断网、流量剧增、用户带宽受限,等等),你的请求要花的时间远比你想象的要长。但setInterval不在乎。它仍然会按定时持续不断地触发请求,最终你的客户端网络队列会塞满Ajax调用。
-
原因三、setInterval不保证执行
与setTimeout不同,你并不能保证到了时间间隔,代码就准能执行。如果你调用的函数需要花很长时间才能完成,那某些调用会被直接忽略。