js定时器

说起 js 中的定时器,我们自然而然的会想到 setTimeout 和 setInterval 这对孪生兄弟,但从语义上可能误认为 setTimeout 和 setInterval 的功能是分离的,他们又自己各自的使用场景,一个是表示多长时间之后执行处理函数,另一个是指每隔多长时间执行一次处理函数,首先说明,这种理解是错误的。下面我们深入理解下这两个函数。

1.理解定时器的执行机制

setTimeout(fn, ms) 及 setInterval(fn, ms) 代表的含义并不是 ms 毫秒之后执行处理函数 fn, 而是在 ms 毫秒后处理函数 fn 被加入 js 的执行队列去排队等待执行。所以,指定 ms 为 1000,并不代表处理函数真的会在 1000ms 后执行,其执行的时机取决于当前执行队列中所有在它之前的等待处理的处理函数及当前正在处理的函数的执行时间之和。
在使用定时器时,绝大多数人都推荐使用 setTimout 实现 interval 的功能,从而代替 setInterval。其原因是因为setInterval 会出现间隔跳过或者连续执行多次处理函数的现象。下面借用一张图描述下:
image.png

下面我将图转化为具体代码:

1
2
3
4
5
6
7
8
9
function handleClick() {
... // 执行代码5ms
setInternal(fn, 200);
... // 执行代码 295ms
} // 大约 300ms

function fn() {
... // 定时器代码,执行大约300多ms;
}
时刻 事件 备注
0ms 开始执行 handleClick 函数
5ms 创建间隔为 200ms 的定时器 200ms 指定时器的处理函数 fn 加入队列的时间间隔
205ms 创建定时器 200ms 后,将定时器处理函数 fn 加入队列 此时 handleClick 仍在执行中,fn 等待执行
405ms fn 再次加入队列,205ms 时加入队列的 fn 正在执行(已经出队列)
605ms 205ms 时加入队列的 fn 仍在执行中,此时队列中还有 405ms 时加入的定时器代码,同一定时器的定时器代码实例只能有一个,此次加入的定时器代码被忽略 定时器代码被跳过
大约630ms 205ms 时加入的定时器代码执行完毕,紧接着 405ms 加入的定时器代码执行 出现连续执行现象

2.使用 setTimeout 替代 setInternal

通过对 setInternal 定时器执行机制的理解,我们发现它存在着间隔跳过及连续执行的弊端。为了保证代码在指定的时间间隔内执行,普遍的做法是使用 setTimeout 替代 setInternal。

1
2
3
4
5
6
7
8
9
// 1. 使用 setInternal
setInternal(fn, 300);

// 2. 使用 setTimeout
function fn() {
// some code...
setTimeout(fn, 300);
}
setTimeout(fn, 300)

3.中央定时器

  • 每个页面在同一时间只需要运行一个定时器
  • 可以根据需要暂停和恢复定时器
  • 删除回调函数的过程变得简单

这个中央定时器在我的开发中没怎么用过,也是偶然接触到的,优点大概是一个定时器周期执行多个处理函数,不需要为每个处理函数定义一个定时器,取消和开始定时器变得非常简单。下面给出一个动画的小栗子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
var timerCtrl = {
timerId: null,
timerCallback: [],
add: function(cb) {
this.timerCallback.push(cb);
return this;
},
start: function() {
if (this.timerId) return;
function runNext() {
for(let i = 0;i < timerCtrl.timerCallback.length; i++) {
if (timerCtrl.timerCallback[i]() === false) {
timerCtrl.timerCallback.splice(i, 1);
i--;
}
}
this.timerId = setTimeout(runNext, 10);
}
runNext();
return this;
},
stop: function() {
if (this.timerId){
clearTimeout(this.timerId);
this.timerId = null;
}
return this;
}
}
var cache = {};
function move(id, direction, distance){
var el = cache[id] || document.getElementById(id);
cache[id] = el;
var num = el.style[direction].replace('px', '');
if (num++ == distance) {
el.innerHTML = id;
return false;
}
el.style[direction] = num+ 'px';
}

timerCtrl
.add(function() { move('box1', 'top', 0); })
.add(function() { move('box2', 'left', 50); })
.add(function() { move('box2', 'top', 50); })
.add(function() { move('box3', 'left', 100); })
.add(function() { move('box3', 'top', 100); })
.add(function() { move('box4', 'left', 150); })
.add(function() { move('box4', 'top', 150); })
.start()

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<div class="container">
<div id="box1"></div>
<div id="box2"></div>
<div id="box3"></div>
<div id="box4"></div>
</div>
</body>
</html>
.container {
position: relative;
}
.container div {
position: absolute;
border: 1px solid red;
height: 50px;
width: 50px;
margin-bottom: 50px;
text-align: center;
line-height: 50px;
}