setTimeout與setInterval概述
setTimeout與setInterval是JavaScript引擎提供的兩個定時器方法,分别用于函數的延時執行和循環調用。前者的主要思想是通過一個定時器,讓函數在計時結束後再執行;後者則是每隔一定的時間,就啟動一次函數的執行。
從原理來看,兩者似乎并不複雜。但由于JavaScript引擎是單線程的,這就讓上述兩個定時器的實際執行變得稍微複雜了一些。下面我們來看一下兩者的運作機制與需要注意的問題。
基本原理
知識鋪墊
單線程模型:由于JavaScript被設計為用在浏覽器環境,而該環境下存在大量可能發生沖突的DOM操作,為了避免進行複雜的沖突處理(可能存在的沖突數量幾乎不可預測),JavaScript的設計者舍棄了java的多線程模型(該模型下,執行引擎同時可以做幾件事,但要進行線程同步),将其設計成了一門單線程語言(執行引擎在同一時間隻做一件事)。
注意:這裡的單線程是指JavaScript的主線程隻有一個。除了這個主線程,JavaScript還有一個I/O線程,通過事件循環來處理I/O問題,但兩者之間相對獨立,不需要進行狀态同步,是以我們仍然可以把JavaScript看成一門單線程語言。
任務隊列:所謂任務隊列,就是用于存儲等待執行的任務的隊列。由于JavaScript是一門單線程語言,如果目前有一個任務需要執行,但JavaScript引擎正在執行其他任務,那麼這個任務就需要放進一個隊列中進行等待。等到線程空閑時,就可以從這個隊列中取出最早加入的任務進行執行(類似于我們去銀行排隊辦理業務。單線程相當于說這家銀行隻有一個服務視窗,一次隻能為一個人服務,後面到的就需要排隊,而任務隊列就是排隊區,先到的就優先服務)。
注意:如果目前線程空閑,并且隊列為空,那每次加入隊列的函數将立即執行。
setTimeout與setInterval
setTimeout(func, delay, args):設定逾時調用。如對于setTimeout(func, 100, args),js引擎會為func函數設定一個計時器,100毫秒後,将func添加到任務隊列等待執行。
setInterval(func, interval, args):設定循環調用。對于語句setInterval(func, 100, args),js引擎每隔100毫秒就會把func添加到任務隊列一次。
相同點:
- 兩者都會加入同一個隊列,等待線程空閑時執行。
- 兩者都無法保證在何時執行回調,因為無法知道線程何時空閑。
不同點
- setTimeout隻會将函數添加到任務隊列一次,而setInterval則是循環往隊列中添加函數。
- setTimeout可以保證函數在指定的時間間隔内不會執行,而setInterval無法保證(有可能出現接近連續執行的情況,後面會分析原因)。
運作機制
setTimeout
setTimeout的運作機制相對簡單,即在執行該語句時,設定一個定時器,定時時間置為所設定的延時,當計時結束後,将傳入的函數加入任務隊列,之後的執行就交給任務隊列負責。
setTimeout函數本身會傳回一個句柄,我們可以在函數執行前通過向clearTimeout傳入該句柄取消函數的執行。示例代碼如下:
function func(message){
;
}
//設定100毫秒後執行func函數
var timer = setTimeout(func, 100, "你好");
function cancel(){
clearTimeout(timer); //取消逾時調用
}
上述代碼将在100毫秒後執行func函數,彈出一個内容為"你好"的對話框。如果在100毫秒内調用了cancel,就可以取消func函數的執行。
setInterval
setInterval本質上就是每隔一定的時間向任務隊列添加回調函數。但setInterval有一個原則:在向隊列中添加回調函數時,如果隊列中存在之前由其添加的回調函數,就放棄本次添加(不會影響之後的計時)。另外也可以通過clearInterval方法移除定時器,使用方法同clearTimeout。
由于setInterval隻負責定時向隊列中添加函數,而不考慮函數的執行,那麼我們考慮一下下面的情況:
假設線程執行完setInterval(func, 100, args)後處于完全空閑狀态(即隻要向任務隊列添加函數就會立即執行)。而func是一個相對複雜的函數,執行該函數需要90毫秒。那麼函數的執行過程就會變成下圖所示:
從圖中可以看到,從上次函數執行完畢,到下次開始執行,之間隻間隔了10毫秒,而不是我們所希望的每隔100毫秒執行一次(因為setInterval隻關注任務添加,不關注任務執行)。
由于上述機制,在很多情況下,setInterval都會遇到一些性能問題。就拿上面的例子來說,我們的本意可能是每隔100毫秒執行一次函數,結果隻等待了10毫秒就又執行了一次。另外,對于複雜的實際情況,setInterval經常出現兩次的執行間隔相差甚遠的情況,對于使用者能感覺到的操作,這會帶來很不好的使用者體驗。是以在實際編碼中,開發者通常會使用setTimeout來模拟實作setInterval效果(下面會有舉例)。
而如果線程一開始是繁忙的,直到150毫秒處才進入空閑狀态(假設func執行時長為10毫秒),那麼實際的運作将變成下圖所示:
這裡在100毫秒處向隊列添加func時,由于線程繁忙,上次添加的func還在隊列中等待,是以直接丢棄本次要添加的函數,但在200毫秒時仍然重新向隊列中添加func。
應用場景
setTimeout
setTimeout主要用于需要進行延時調用的場景中。如之前一篇文章介紹的js基礎之函數的節流與防抖,就是setTimeout典型的應用場景。此外,由于setInterval存在的性能問題,在實際的編碼中,開發人員通常會使用setTimeout來模拟setInterval,以防止出現函數連續執行的情況。如對于下面的代碼:
function func(args){
//函數本身的邏輯
...
}
var timer = setInterval(func, 100, args);
我們可以通過以下代碼來實作:
var timer;
function func(args){
//函數本身的邏輯
...
//函數執行完後,重置定時器
timer = setTimeout(func, 100, args);
}
timer = setTimeout(func, 100, args);
利用setTimeout保證在指定的時間内不會執行的特點,我們可以在執行完上次的回調函數後,重置定時器,實作循環執行func的效果,并且從上次執行完畢到下次執行開始,至少會經過100毫秒。這在實際的編碼中通常會帶來較大的性能提升,同時函數的執行間隔也會相對穩定。
setInterval
盡管存在上述性能問題,setInterval的使用場景相對較少,但當所使用的接口來自外部(即回調函數本身無法修改)時,就必須通過setInterval來實作循環執行了。此外,對于動畫效果來說,我們通常會希望動畫運作的更加平滑(也就是希望函數運作得更頻繁),這時使用setInterval往往更加流暢,具體請參考之前的文章使用原生js實作簡單動畫效果。
除了這類情況,開發者一般不會使用setInterval方法進行循環調用。
補充說明
setTimeout與setInterval的第一個參數可以是一個匿名函數,也可以是一個函數名,或者是一個字元串,如下面的寫法都是合法的:
function func(msg){
...
}
//傳入回調函數名
setTimeout(func, 100, "夕山雨");
//傳入匿名函數
setTimeout(function(name){
...
}, 100, "夕山雨");
//傳入字元串,js引擎會将其解析為函數體
setTimeout("", 100);
但是傳入如下的格式就可能報錯:
setTimeout(func("夕山雨"), 100);
因為這種寫法實際上是先調用func函數,然後再将傳回值添加到任務隊列。如果func的傳回值不是函數(或可執行的字元串),那麼程式就會報錯;如果傳回值是函數,則會将傳回的函數添加到任務隊列。該情況可以寫成下面的形式:
//将其作為字元串傳入,就可以被正确解析
setTimeout("func('夕山雨')", 100);
此外,當給setTimeout傳入的延遲時間為0時,并不代表回調函數會立即執行。實際上浏覽器規定的有一個預設的最短計時時間,對于現代浏覽器,這個時間一般為4毫秒(老版本的浏覽器則會更長一些)。也就是說,即使傳入的延遲時間為0,浏覽器也會至少在4毫秒後才會執行。
上述補充說明同樣适用于setInterval。
總結
setTimeout與setInterval都是通過一個定時器控制回調函數的執行,但由于javascript單線程的特點,兩者都不能準确控制函數的執行時間點,這點還請開發者注意。如果函數隻需要執行一次,很顯然我們會使用setTimeout來實作;如果是循環執行的情況,如果我們希望函數執行頻率不那麼高,并且間隔更穩定,通常是使用setTimeout模拟實作setInterval效果。
總的來說,雖然都被用于函數延遲執行,但兩者的運作機制有本質上的差別,是以在使用的時候請注意區分。