30天學習計劃 js忍者祕籍 第8章 馴服執行緒和定時器

weixin_34019929發表於2016-09-30

9.26-9.30

第8章 馴服執行緒和定時器

定時器可以在js中使用,但它不是js的一項功能,如果我們在非瀏覽器環境中使用js,很可能定時器就不存在了,需要自己實現自己的定時器版本。

定時器提供了一種讓一段程式碼在一定毫秒之後,再非同步執行的能力。由於js是單執行緒的特性(同一時間只能執行一處js程式碼),定時器提出了一種跳出這種限制的方法,以一種不太直觀的方式來執行程式碼。

8.1 定時器和執行緒是如何工作的

8.1.1 設定和清除定時器

js提供了兩種方式,用於建立定時器以及兩個相應的清除方法(刪除)。這些方法是window物件(全域性上下文)上的方法。

id = setTimeout(fn,delay) 啟動一個定時器,在一段時間(delay)之後執行傳入的callback,並返回該定時器的唯一標識

clearTimeout(id) 如果定時器還未觸發,傳入定時器標識即可取消(清除)該定時器

id = setInterval(fn,delay) 啟動一個定時器,在每隔一段時間之後都執行傳入的callback,並返回該定時器的唯一標識

clearInterval(id) 傳入間隔定時器標識,即可取消該間隔定時器

js定時器的延遲時間是不能保證的,原因和js執行緒的本質有很大關係。

8.1.2 執行執行緒中的定時器執行

在Web worker可用之前,瀏覽器中的所有js程式碼都是在單執行緒中執行的,是的,只有一個執行緒。

處理程式在執行時必須進行排隊執行,並且一個處理程式並不能中斷另外一個處理程式的執行。

8.1.3 timeout與interval之間的區別

示例8.1 兩種建立重複定時器的方式

setTimeout(function repeatMe(){  //定義一個timeout定時器,每10毫秒都重新呼叫自己

//code

setTimeout(repeatMe,10)

},10)

setInterval(function(){  //定義一個interval定時器,每10毫秒都觸發一次

//code

},10)

在setTimeout()程式碼中,要在前一個callback回撥執行結束並延遲10秒以後,才能再次執行setTimeout()。

而setInterval()則是每隔10毫秒就嘗試執行callback回撥,而不關注上一個callback是何時執行的。

.js引擎是單執行緒執行,非同步事件必須要排隊等待才能執行

.如果無法立即執行定時器,該定時器會被推遲到下一個可用的執行時間點上(可能更長,但不會比指定的延遲時間更少)。

.如果一直被延遲,到最後,interval間隔定時器可能會無延遲執行,並且同一個interval處理程式的多個例項不能同時進行排隊。

.setTimeout()和setInterval()在觸發同期的定義上是完全不同的。

8.2 定時器延遲的最小化及其可靠性

現代瀏覽器通常無法實現1毫秒粒度的可持續間隔,某些瀏覽器的實現可以非常接近。

當我們對setInterval()設定0毫秒的延遲時,ie瀏覽器定時器的callback回撥只會執行一次,和使用setTimeout效果一樣。

瀏覽器不保證我們指定的延遲間隔,雖然可以指定特定的延遲值,但其準確性卻並不總是能夠保證,尤其是在延遲值很小的時候。

8.3 處理昂貴的計算過程

js的單執行緒本質可能是js複雜應用程式開發中的最大“陷阱”。在js執行的時候,頁面渲染的所有更新操作都要暫停。

如果要保持介面有良好的響應能力,減少執行時間超過幾百毫秒的複雜操作,將其控制在可管理狀態是非常必要的。

如果一段指令碼的執行時間超過5秒,有些瀏覽器將彈出一個對話方塊警告使用者該指令碼“無法響應”。iPhone上的瀏覽器,將預設終止執行時間超過5秒鐘的指令碼。

作為定時器,它在一段時間之後,可以有效暫停一段js程式碼的執行,定時器還可以將程式碼的各個部分,分解成不會讓瀏覽器掛掉的碎片。

考慮到這一點,我們可以將強迴圈和操作轉化為非阻塞操作。

示例8.2 一個長時間執行的任務

var tbody = document.getElementsByTagName('tbody')[0];

for(var i=0; i<20000; i++){

var tr = document.createElement('tr');

for(var t=0; t<6; t++){

var td = document.createElement('td');

td.appendChild(document.createTextNode(i+','+t));

tr.appendChild(td);

}

tbody.appendChild(tr)

}

上例建立了240000個DOM節點,並使用大量的單元格來填充一個表格。這是非常昂貴的操作,明顯會增加瀏覽器的執行時間,從而阻止正常的使用者互動操作。

我們可以引入定時器,在程式碼執行的時候定期暫停休息

示例8.3 利用定時器分解長時間執行的任務

var tbody = document.getElementsByTagName('tbody')[0];

var rowCount = 20000;

var divideInto = 4;

var chunkSize = rowCount/divideInto;

var iteration = 0;

setTimeout(function generateRows(){

var base = (chunkSize)*iteration;

for(var i=0; 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)

}

tbody.appendChild(tr);

}

iteration++;

if(iteration

setTimeout(generateRows,0)

}

},0);

上例將操作分成四步小操作,每個操作建立自己的DOM節點。這些較小的操作,則不太可能讓瀏覽器掛掉。

8.4 中央定時器控制

使用定時器可能出現的問題是對大批量定時器的管理。

同時建立大量的定時器,將會在瀏覽器中增加垃圾回收任務的可能性。垃圾回收就是瀏覽器遍歷其分配過的記憶體,並試圖刪除沒有任何應用的未使用物件的過程。定時器是一個特殊的問題,因為通常它們是在js單執行緒引擎之外的流程中進行管理。有些瀏覽器可以很好地處理這種情況,有些瀏覽器的垃圾回收週期則很長。一個動畫在某個瀏覽器中很漂亮、很流暢,但在另外一個瀏覽器中卻很卡頓。

在多個定時器中使用中央定時器控制,可以帶來很大的威力和靈活性。

.每個頁面在同一時間只需要執行一個定時器。

.可以根據需要暫停和恢復定時器。

.刪除回撥函式的過程變得很簡單。

示例8.4 管理多個處理程式的中央定時器控制

test suite

#box{position:absolute;width:60px;height:40px;border:1px solid #060; text-align:center;}

Hello!

var timers={

timerID:0,

timers:[],

add:function(fn){

this.timers.push(fn);

},

start:function runNext(){

if(this.timerID) return;

(function(){

if(timers.timers.length > 0){

for(var i=0; i

if(timers.timers[i]() === false){

timers.timers.splice(i,1);

i--

}

}

timers.timerID = setTimeout(runNext,0)

}

})()

},

stop:function(){

clearTimeout(this.timerID);

this.timerID=0;

}

}

var box = document.getElementById('box'),x=0,y=20;

timers.add(function(){

box.style.left = x + 'px';

if(++x>50) return false;

})

timers.add(function(){

box.style.top = y+'px';

y+=2;

if(y>120) return false;

})

timers.start();

一開始,所有的回撥函式都儲存於一個名為timers的陣列中,還包含當前定時器的一個ID,這些變數是定時器唯一需要維護的內容。

add()方法接受一個callback回撥,並簡單將其新增到timers陣列中。

start()方法首先確認沒有定時器在執行(通過檢查timerID是否有值),如果確認沒有定時器在執行,立即執行一個即時函式來開啟中央定時器。

在即時函式內,如果註冊了處理程式,就遍歷執行每個處理程式。如果有處理程式返回false,我們就從陣列中將其刪除,最後進行下一次排程。

以這種方式組織定時器,可以確保回撥函式總是按照新增的順序進行執行。而普通的定時器通常不會保證這種順序,有可能後面的一個處理程式在前面就執行了。

這種方式的定時器組織,對於大型應用程式或任何形式的js動畫來說都是至關重要的。

8.5 非同步測試

示例:簡單的非同步測試套件

(function(){

var queue = [],paused=false;

this.test = function(fn){

queue.push(fn);

runTest();

}

this.pause = function(){

paused = true;

}

this.resume = function(){

paused = false;

setTimeout(runTest,1)

}

function runTest(){

if(!paused && queue.length){

queue.shift();

if(!paused) resume();

}

}

})()

示例中,傳遞給test()方法的每個函式,最多隻包含一個非同步測試。它們的非同步性由pause()和resume()的使用所定義,這兩個方法分別在非同步事件之前或之後進行呼叫。這段程式碼是一種確保讓包含非同步行為的函式,以特定的順序進行執行的方式。

該佇列唯一的目的是在等待執行的時候,出列一個函式並進行執行。否則,就完全停止執行一個時間間隔。

這段程式碼,強制測試套件以純粹非同步方式進行執行,但同時又保證了測試執行的順序。

相關文章