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();
}
} //運行測試函數
})(); //這段代碼只是將異步的代碼按添加的順序執行 已測試 即在等待執行的時候,執行隊列中的第一個否則就完全停止