js中的线程和定时器
- 在js中定时器是一个非常强大但经常会被错误使用的一个特性,如果能够正确使用定时器那么就会给开发人员带来非常多的好处
- 定时器提供一种让一段代码在一定毫秒之后,再 异步执行的能力,由于js是单线程的(同一时间只能执行一处的javascript代码),定时器提供一种跳过这种限制的方法。
1. 定时器和线程是如何工作的
1.1 设置和清楚定时器
js提供了两种方式,用于创建定时器以及相应两种方法(删除),这些方法都是windows对象上的方法
js中操作定时器的方法 :
方法 | 格式 | 描述 |
---|---|---|
setTImeout | id=setTimemout(fn,time) | 启动一个定时器,在一段时间(time)之后执行传入的回调函数 fn,返回一个定时器id用于clear |
clearTimeout | clearTimeout(id) | 如果定时器还没触发,传入id就可以取消该定时器 |
setinterval | id=setinterval(fn,time) | 启动一个定时器,在每隔一段时间(time)之后执行传入的回调 函数fn,并且返回一个定时器id涌入clear |
clearinterval | clearinterval(id) | 传入间隔定时器标识,即清除该定时器 |
这里需要提示一下,js中的延迟时间是不能保证的,原因和js的单线程有很大关系
1.2 执行进程中的定时器运行
js中的单线程造成的结果是:异步事件的处理程序,如果用户界面和定时器,在线程中没有代码执行的时候才会执行,也就是说这些程序在执行时必须要排队执行,并且一个处理程序不能中断另一个。例如当一个异步事件触发时(如鼠标单击‘click’,定时器触发,甚至是XMLHttpRequest完成事件),它就会排队,并且当线程空闲时它才执行,实际上每一个浏览器的排队机制是不同的。
- 在0毫秒时,启动一个10毫秒的setTimeout以及一个10毫秒的setinterval
- 在6毫秒时,执行鼠标点击触发click
- 在10毫秒时,定时器和间隔定时器触发
- 但是第一个js代码块要执行18毫秒
- 直到这段时间内相继出发了 鼠标单击click时间 setTimeout 以及两次setinterval事件,由于这个时间段内有别的代码正在执行,所以这些时间中的处理程序就不能执行,所以就开始排队
- 直到鼠标单击事件结束 在timeout处理程序执行时(原本我们要在10毫秒执行的),注意在30秒的时候又触发了一次间隔定时器,但是由于之前已经有一个interval代码正在排队,所以这次的处理程序就不会执行,按通俗易懂的话就是等到线程中没有处理程序时,才会将其添加到队伍中,浏览器不会对特定的interval处理程序的多个实例进行排队,
- 在34毫秒,timeout执行结束,队列中的interval处理程序开始执行 由于该程序要执行6毫秒,所以在执行到40毫秒又触发interval时间,进入队列,所以到目前为止进入队列的interval只有10毫秒和40豪秒触发的
- 在42秒开始执行40秒触发的之后 因为运行时间为6毫秒所以 之后没次的interval时间都会在每10毫秒运行
正如我们所看到的interval中的几个处理程序完全被“挤没了”
1.3timeout与interval的区别
乍一看感觉两者并无什么明显区别
<script type="text/javascript">
setTimout(function(){
//执行功能
settimeout(repeatMe,10); //重新调用自己
})
setInterval(function(){
//执行功能
},10) //定义一个Interval定时器,每隔10秒触发一次
<script>
在上述代码中,两者功能看似是相同的,实际上是不同的,其实在setTimeout()代码中,要在前一个的执行功能结束后才会添加一个定时器setTimeout(),而setInterval()则是每隔10毫秒,就尝试回调函数,而不管上一个回调函数是否执行完毕。
所以有下述结论
1. 在js是单线程执行,异步事件必须排队等待执行。
2. 如果无法立即执行定时器,改定时器就会被推迟到下一个进程空闲的时间点上执行。
3. setTimeout()和setInterval()的执行周期是完全不同的。
4.
2. 定时器用法1:处理大量计算过程
js单线程的本质可能是用js实现复杂操作的一个陷阱,在js执行繁忙的时候,浏览器的用户交互,最好的情况是稍有缓慢,最差的情况则是反应迟钝,这随时可能导致浏览器随时挂掉,因为在js脚本执行的时候页面所有渲染都要停止,但是如果我们要同时操作上千个DOM的时候,就会引起页面卡顿,这个时候定时器就来救我们了 ,它可以暂停一段代码,让其在空闲的时间去执行
一下是一个例子
<table><tbody></tbody></table>
<script type="text/javascript">
var tbody=document.getElementByTagName("tbody")[0];//找到tbody元素
for(var i=0;i<20000;i++){
var tr=document.createElement("tr")//创建大量行元素
for(var t=0;t<6;t++){
var td=document.createElemnt("td")//创建列
td.appendChild(doument.creatTextNode(i+","+t))//每个列都有个文本节点
tr.appendChild(td);//将列元素塞入行
}
tbody.appendChild(tr);//将每一行插入tbody
}
</script>
在本例中我们创建了240000个DOM节点,并使用大量单元格来填充一个表格,这是非常大量的DOM操作,明显会增加浏览器的执行时间,从而阻断正常的用户交互操作,定时器的作用就来了,在代码执行的时候可以暂停休息,修改后的代码如下
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<table>
<tbody>
</tbody>
</table>
</body>
<script type="text/javascript">
var rowCount=20000;//有多少行
var divideInto=4; //分几步
var chunkSize=rowCount/divideInto; //每步执行多少行
var iteration=0; //当前步
var table=document.getElementsByTagName("tbody")[0];
setTimeout(function generateRows(){
var base=(chunkSize)*iteration; //计算上次中断地方
for(var i=0;i<chunkSize;i++){
var tr=document.createElement("tr");
for(var t=0;t<6;t++){
var td=document.createElement("td");
td.appendChild(document.createTextNode((i+base)+","+t+","+iteration));
tr.appendChild(td);
}
table.appendChild(tr);
}
iteration++;//步数增加
if(iteration<divideInto){
setTimeout(generateRows,0); //下一阶段
}
},0);
</script>
</html>
3.定时器用法2:中央定时器控制
在使用定时器出现的问题就是对大批定时器的管理,这在处理动画效果时尤为重要,所以在操纵大量定时器属性的时候,我们需要用一种方式来管理他们
同时管理多个定时器会有多问题,如如何保留大量间隔定时器的应用,然后还得取消他们(尽管可以使用闭包这种方法),而且还干扰了浏览器的正常运行(这还要看不同浏览器的垃圾处理机制)。
在一个简单轮播图中 自动播放与运动函数 两个定时器叠加在一起,可能会发现在一个浏览器运行良好,可到了另一个浏览器里 却变的非常的卡顿,甚至是崩掉。所以现在动画都使用一种为中央定时器控制的技术
- 每个页面在同一时间只需要运行一个定时器
- 可以根据需要恢复和暂停定时器
- 删除回调函数过程变得很简单
<script type="text/javascript">
var timer = { //一个定时器控制对象
timerID: 0, //记录状态
timers: [],
add: function(fn) {
this.timers.push(fn);
}, //添加定时器函数
start: function() {
if(this.tiemrID) return; //若已有定时器运行 返回
(function runNext() { //若定时器序列中有定时器
if(timer.timers.length>0){ //寻找序列中的定时器 看那个执行完毕
for(var i=0;i<timer.timers.length;i++){
if(timer.timers[i]()===false){
timer.timers.splice(i,1);
i--; //清除定时器 i--
}
} //直到序列中定时器被清除完毕,结束
timer.timerId=setTimeout(runNext,0); //进行下一次调用
}
})(); //闭包保存所有属性值
}, //开启函数
stop:function(){
clearTimeout(this.timerID);
this.timerID=0; //停止函数
}
};
</script>
这里的核心就是start方法,首先确认没有定时器在运行,如果没有就立即执行一个即时函数(闭包保存属性)来开启中央处理定时器 ,在这个即时函数内,如果有处理程序,就遍历每一个,如果有某个程序完成返回false我们就从数组中删除。然后进入下一次调用
我们来测试一下
CTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div id="box" style="position: relative;">
hello!
</div>
</body>
<script type="text/javascript">
var timer = { //一个定时器控制对象
timerID: 0, //记录状态
timers: [],
add: function(fn) {
this.timers.push(fn);
}, //添加定时器函数
start: function() {
if(this.tiemrID) return; //若已有定时器运行 返回
(function runNext() { //若定时器序列中有定时器
if(timer.timers.length > 0) { //寻找序列中的定时器 看那个执行完毕
for(var i = 0; i < timer.timers.length; i++){
if(timer.timers[i]() === false) {
timer.timers.splice(i, 1);
i--; //清除定时器 i--
}
} //序列中定时器被清除完毕,结束
timer.timerID = setTimeout(runNext, 0);
}
})(); //闭包保存所有属性值
}, //开启函数
stop: function() {
clearTimeout(this.timerID);
this.timerID = 0; //停止函数
}
};
var box = document.getElementById("box"),
x = 0,
y = 20;
timer.add(function() {
box.style.left = x + "px";
if(++x > 500) return false;
});
timer.add(function() {
box.style.top = y + "px";
y += 2;
if(y > 500) return false;
});
timer.start();
</script>
</html>
这种方式组织定时器,可以确保回调函数总是按照顺序执行,而普通的定时器 并不会这样。
这种方法还有另一种作用
4.定时器用法3:异步测试
当我们要对还没完成操作的代码执行测试的时候,我们需要将这种测试从测试套件中分离出来,以便测试是否异步
(function() {
var queue = [],
paused = falsed; //状态表
this.test = function(fn) {
queue.push(fn); //定义测试函数
runTest();
};
this.paused = function() {
paused = true; //定义停止测试函数
}
this.resume = function() {
paused = false;
setTimeout(runTest,1); //定义恢复测试函数
}
function runTest(){
if(!paused&&queue.length){
queue.shift()();
if(!paused)
resume();
}
} //运行测试函数
})(); //这段代码只是将异步的代码按添加的顺序执行 已测试 即在等待执行的时候,执行队列中的第一个否则就完全停止