你真的瞭解setTimeout和setInterval嗎?

博客園的代碼排版真難用,編輯時候是好的,一保存就是亂了——本文也同時發表在我另外一獨立博客上 你真的瞭解setTimeout和setInterval嗎?,能夠移步至這裏吧javascript

setTimeout和setInterval的基本用法咱們不談,無非是1.指定延遲後調用函數,2.以指定週期調用函數java

讓咱們想象一個意外狀況,好比說下面的setIntervaljquery

setInterval(function(){
    func(i++);
},100)

  

咱們以每100毫秒調用一次func函數,若是func的執行時間少於100毫秒的話好辦,在遇到下一個100毫秒前就可以執行完:git

但若是func的執行時間大於100毫秒,該觸發下一個func函數時以前的尚未執行完怎麼辦?(前提是你要知道javascript只有單線程,不存在同時執行致命一說,纔會有這個問題)。答案以下圖所示,那麼第二個func會在隊列(這裏的隊列是指event loop,在下文中會詳細提到)中等待,直到第一個函數執行完github

若是第一個函數的執行時間特別長,在執行的過程當中本應觸發了許多個func怎麼辦,那麼全部這些應該觸發的函數都會進入隊列嗎?web

 

不,只要發現隊列中有一個被執行的函數存在,那麼其餘的通通忽略。以下圖,在第300毫秒和400毫秒處的回調都被拋棄,一旦第一個函數執行完後,接着執行隊列中的第二個,即便這個函數已經「過期」好久了。ajax

還有一點,雖然你在setInterval的裏指定的週期是100毫秒,但它並不能保證兩個函數之間調用的間隔必定是一百毫秒。在上面的狀況中,若是隊列中的第二個函數時在第450毫秒處結束的話,在第500毫秒時,它會繼續執行下一輪func,也就是說這之間的間隔只有50毫秒,而非週期100毫秒數組

那若是我想保證每次執行的間隔應該怎麼辦?用setTimeout,好比下面的代碼:瀏覽器

var i =1var timer = setTimeout(function(){ 
    alert(i++) 
    timer = setTimeout(arguments.callee,2000)
},2000)

  

上面的函數每2秒鐘遞歸調用本身一次,你能夠在某一次alert的時候等待任意長的時間(不按「肯定」按鈕),接下來不管你何時點擊「肯定」, 下一次執行必定離此次肯定相差2秒鐘的app

下面上下兩段代碼雖然看上去功能一致,但實際並不是如此,緣由就是我上面所說

setTimeout(function repeatMe(){
/* Some long block of code... */ setTimeout(repeatMe,10);
},10);
setInterval(function(){
/* Some long block of code... */
},10);

  

setTimeout除了作定時器外還能幹什麼用?

很是多,好比說:在處理DOM點擊事件的時候一般會產生冒泡,正常狀況下首先觸發的是子元素的handler,再觸發父元素的handler,若是我想讓父元素的handler先於子元素的handler執行應該怎麼辦?那就用setTimeout延遲子元素handler若干個毫秒執行吧。問題是這個「若干個」毫秒應該是多少?能夠是0

你可能會疑惑若是是0的話那不是當即執行了嗎?不,看下面一道題目

(function(){
    setTimeout(function(){
        alert(2);
},0); alert(1);
})()

  

先彈出的應該是1,而不是你覺得「當即執行」的2。

setTimeout,setInterval都存在一個最小延遲的問題,雖然你給的delay值爲0,可是瀏覽器執行的是本身的最小值。HTML5標準是4ms,但並不意味着全部瀏覽器都會遵循這個標準,包括手機瀏覽器在內,這個最小值既有可能小於4ms也有可能大於4ms。在標準中,若是在setTimeout中嵌套一個setTimeout, 那麼嵌套的setTimeout的最小延遲爲10ms。

下面咱們聊聊setTimeout和線程的一些關係

如今我有一個很是耗時的操做(以下面的代碼,在table中插入2000行),我想計算這個操做所耗的時間應該怎麼辦?你以爲下面這個用new Date來計算的方法怎麼樣:

var t1 =+newDate();
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);
}
var t2 =+newDate(); console.log(t2 - t1);

  

若是你嘗試運行起來就會發現問題,在這2000行尚未渲染出來的時候,控制檯就已經打印出來了時間,這兩個時間差並不是偏差所致(可能這個操做須要5秒,甚至10秒以上),可是打印出來的時間只有1秒左右,這是爲何?

由於Javascript是單線程的(這裏不談web worker),也就是說瀏覽器不管何時都只有一個JS線程在運行JS程序。或許是由於單線程的緣故,也同時由於大部分觸發的事件是異步的,JS採用一種隊列(event loop)的機制來處理各個事件,好比用戶的點擊,ajax異步請求,全部的事件都被放入一個隊列中,而後先進先出,逐個執行。這也就解釋了開頭setInterval的那種狀況。

另外一方面,瀏覽器還有一個GUI渲染線程,當須要重繪頁面時渲染頁面。但問題是GUI渲染線程與JS引擎是互斥的,當JS引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到JS引擎空閒時當即被執行。

因此,在腳本中執行對界面進行更新操做,如添加結點,刪除結點或改變結點的外觀等更新並不會當即體現出來,這些操做將保存在一個隊列中,待JavaScript引擎空閒時纔有機會渲染出來.

因此,上面的那個例子中算出的時間只是javascript執行的時間,在這以後,GUI線程纔開始渲染,而此時計時已經結束了。那麼如何你能計算出正確時間呢?在結尾添加一個setTimeout

var t1 =+newDate();
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);
} setTimeout(function(){
var t2 =+newDate(); console.log(t2 - t1);
},0)

  

這樣能讓操縱DOM的代碼執行完後不至於當即執行t2 - t1,而在中間空隙的時間剛好容許瀏覽器執行GUI線程。渲染完以後,才計算出時間。

下面這個例子也是一樣的道理,能夠如何改進才能看到顏色的改變呢?留做做業吧:

function run(){
var div = document.getElementsByTagName('div')[0]
for(var i=0xA00000;i <0xFFFFFF;i++){ div.style.backgroundColor ='#'+i.toString(16)
}
}

  

setInterval有一個很重要的應用是javascript中的動畫

舉個例子,假設咱們有一個正方形div,寬度爲100px, 如今想讓它的寬度在1000毫秒內增長到300px——很簡單,算出每毫秒內應該增長的像素,再按每毫秒爲週期調用setInterval增加

var div = $('div')[0];
var width = parseInt(div.style.width,10);

var MAX =300, duration =1000;
var inc = parseFloat((MAX - width)/ duration );

function animate (id){ width += inc;
if(width >= MAX){ clearInterval(id); console.timeEnd("animate");
} div.style.width = width +"px";
} console.time("animate");
var timer = setInterval(function(){ animate(timer);
},0)

  

代碼中利用console.time來計算時間所花費的時間——實際上花的時間是明顯大於1000毫秒的,爲何?由於上面說到最小週期至少應該是4ms,因此每一個週期的增加量應該是沒每毫秒再乘以四

var inc = parseFloat((MAX - width)/ duration )*4;

  

若是你有心查看jquery的動畫源碼的話,你能發現源碼的時間週期是13ms,這是我不解的地方——若是最求流暢的動畫效果來講,每秒(1000毫秒)應該是60幀,這樣算下來每幀的時間應該是16.7毫秒,在這裏我把每幀定義爲完成一個像素增量所花的時間,也就是16毫秒(毫秒不容許存在小數)是讓動畫流暢的最佳值。哪位朋友能夠告訴jquery的13這個值是如何來的?

不管你如何優化setInterval,偏差是始終存在的。但其實在HTML5中,有一個實踐動畫的最佳途徑requestAnimationFrame。這個函數能保證能以每幀來執行動畫函數。好比上面的例子就能夠改寫爲:

//init some values
var div = $('div')[0].style;
var height = parseInt(div.height,10);
var seconds =1;

//calc distance we need to move per frame over a time
var max =300;var steps =(max- height)/ seconds /16.7;

//16.7ms is approx one frame (1000/60)
//loop
function animate (id){ height += steps;//use calculated steps div.height = height +"px";
if(height < max){ requestAnimationFrame(animate);
}
} animate();

  

關於這個函數和它對應的cancel函數,或者是polyfill就不在這延伸了,有興趣的同窗能夠本身查找資料瞭解。

這種狀況下一般會有多個計時器同時運行,若是同時大量計時器同時運行的話,會引發一些個問題,好比如何回收這些計時器?jquery的做者John Resig建議創建一個管理中心,它給出的一個很是簡單的代碼以下:

var timers ={                               
  timerID:0,                                           
  timers:[],                                           
  add:function(fn){
  this.timers.push(fn);
  }, start:function(){
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,0);
}
})();
}, stop:function(){ clearTimeout(this.timerID);this.timerID =0;
}
};

  

注意看中間的start方法:他把全部的定時器都存在一個timers隊列(數組)中,只要隊列長度不爲0,就輪詢執行隊列中的每個子計時器,若是某個子計時器執行完畢(這裏的標誌是返回值是false),那就把這個計時器踢出隊列。繼續輪詢後面的計時器。

上面描述的整個一輪輪詢就是runNext,而且遞歸輪詢,一遍一遍的執行下去timers.timerID = setTimeout(runNext, 0)直到數組爲空。

注意到上面沒有使用到stop方法,jquery的動畫animate就是使用的是這種機制,不過更完善複雜,摘一段jquery源碼看看,好比就相似的runNext這段:

// /src/effects.js:674
jQuery.fx.tick =function(){
var timer, timers = jQuery.timers, i =0; fxNow = jQuery.now();
for(; i < timers.length; i++){ timer = timers[ i ];
// Checks the timer has not already been removed
if(!timer()&& timers[ i ]=== timer ){ timers.splice( i--,1);
}
}
if(!timers.length ){ jQuery.fx.stop();
} fxNow =undefined;
};

// /src/effects.js:703 jQuery.fx.start =function(){
if(!timerId ){ timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
}
};

  

不解釋,和上面的那段已經很是相似了,有興趣的同窗能夠在github上閱讀整段effect.js代碼。

最後setTimeout的應用就是總所周知的,來處理由於js處理時間過長形成瀏覽器假死的問題了。這個技術在《JavaScript高級程序設計》中已經闡述過了(沒有誰沒有讀過這本書吧)。簡單來講,若是你的循環1.每一次處理不依賴上一次的處理結果;2.沒有執行的前後順序之分;3.(呃,忘了)。由於手頭上暫時找不到這本書,在網上找了一段相似的代碼做爲拋磚引玉做爲結尾吧,有興趣的同窗能夠去回顧這段:

function chunk(array, process, context){
     setTimeout(function(){
var item = array.shift(); process.call(context, item);
if(array.length >0){ setTimeout(arguments.callee,100);
}),100);
}

  

chunk()函數的用途就是將一個數組分紅小塊處理,它接受三個參數:要處理的數組,處理函數以及可選的上下文環境。每次函數都會將數組中第一個對象取出交給process函數處理,若是數組中還有對象沒有被處理則啓動下一個timer,直到數組處理完。這樣可保證腳本不會長時間佔用處理機,使瀏覽器出一個高響應的流暢狀態。

參考資料:

相關文章
相關標籤/搜索