第六章、驯服线程和定时器

定时器和线程是如何工作的

JavaScript提供了两种方式,用于创建定时器以及两个相应的清除方法。这些方法都是window对象上的。

// 在一段时间(delay)之后执行传入的fn方法,并返回该定时器的唯一标识
var id = setTimeout(fn,delay);
// 在定时器还未触发时,取消定时器
clearTimeout(id)

// 在每间隔一段时间(delay)之后都执行传入的fn方法,并返回该定时器的唯一标识
var id = setInterval(fn,delay);
// 取消间隔定时器
var id = clearInterval(id)
1
2
3
4
5
6
7
8
9

由于JavaScript是单线程的,在特定的时间点只能运行一个执行代码,而且也无法确定定时器处理程序到底是在什么时候执行,当一个异步事件发生时(鼠标单击,定时器触发,或者是ajax返回函数),它会进行排队,在线程空闲时才进行执行,并且每个浏览器的排队机制是不同的,浏览器不会对来自同一个setInterval()的多个回调进行排队,同一时刻,将只会有一个来自同一setInterval()的回调在队列中。 interval间隔定时器并不是周期执行的timeout定时器,通过下面代码可以看出差异。

setTimeout(function repeat() {
	/*主内容代码*/
	setTimeout(repeat,10);
},10);

setInterval(function () {
    /*主内容代码*/
},10);
1
2
3
4
5
6
7
8

在setTimeout()代码中,要在前一个主题内容代码执行结束并延迟10ms的时间后,才能再次执行setTimeout()。 而setInterval()代码中,每间隔10ms就尝试执行主内容代码,并不会关注上一次setInterval()中的主内容代码是如何执行的。 浏览器无法保证我们指定的延迟间隔,尤其是在间隔时间非常小的情况下,因为执行回调函数和代码本身也要花费时间。

处理昂贵的计算过程

下面代码会创建240000个DOM节点,并使用大量单元格来填充一个表格。

var tbody = document.getElementsByTagName("tbody")[0];
for(var i=0;i<20000;i++){
    var tr = document.createElement("tr");
    for(var j=0;j<6;j++){
        var td = document.createElement("td");
        td.appendChild(document.createTextNode(i+","+j));
        tr.appendChild(td);
    }
    tbody.appendChild(tr);
}
1
2
3
4
5
6
7
8
9
10

执行类似上述计算昂贵的代码,浏览器往往会卡顿很长时间,将这些操作分隔,定期让代码中断,并记录中断的地方,间隔一定时间再调度下一阶段。

var row = 20000;
var divede = 10;  // 分隔的数量
var cur = row / divede;  // 每次执行的次数
var now = 0;  // 执行的阶段
var tbody = document.getElementsByTagName("tbody")[0];
setTimeout(function goNow() {
    var go = cur * now;  // 记录上次中断结束的地方
    for(var i=0;i<cur;i++){
        var tr = document.createElement("tr");
        for(var j=0;j<6;j++){
            var td = document.createElement("td");
            td.appendChild(document.createTextNode((i+go)+","+j+","+now));
            tr.appendChild(td);
        }
        tbody.appendChild(tr);
    }
    now++; // 调度下一阶段
    if(now<divede){
        setTimeout(goNow,0);
    }
},0);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

中央定时器控制

var timers = {  // 定义中央定时器控制对象
    timerID:0,  // 当前正在执行的定时器,为0则表示没有定时器在执行
	timers: [],  // 保存定时器需要执行的所有回调函数
	add:function (fn) {  // 定义add方法,添加回调函数
		this.timers.push(fn);
    },
	start:function (time) {  // 执行定时器
        if(this.timerID) return;  // 没有定时器时,执行一个即时函数来开启中央定时器
		(function runNext() {  // 执行当前定时器
			if(timers.timers.length>0){  // 遍历所有回调函数进行执行
			    for(var i=0;i<timers.timers.length;i++){
			        if(timers.timers[i]()===false){  // 当某个回调函数不需要在执行时,将其删除
			            timers.timers.splice(i,1);
			            i--;
			        }
			    }
			    timers.timerID = setTimeout(runNext,time); // 等待time时间再次调用定时器
			}
        })();
    },
	stop:function () {  // 清除定时器
		clearTimeout(this.timerID);
		this.timerID =0;
    }
};

// 测试中央控制器
var box = document.getElementById("box"),x=0,y=0;
timers.add(function () {
	box.style.left=x+"px";
	if((x+=1)>200)return false;
});
timers.add(function () {
    box.style.top=y+"px";
    if((y+=1)>200)return false;
});
timers.start(10);
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