setTimeout與setInterval是JavaScript引擎提供的兩個(gè)定時(shí)器方法,分別用于函數(shù)的延時(shí)執(zhí)行和循環(huán)調(diào)用。前者的主要思想是通過一個(gè)定時(shí)器,讓函數(shù)在計(jì)時(shí)結(jié)束后再執(zhí)行;后者則是每隔一定的時(shí)間,就啟動(dòng)一次函數(shù)的執(zhí)行。
從原理來看,兩者似乎并不復(fù)雜。但由于JavaScript引擎是單線程的,這就讓上述兩個(gè)定時(shí)器的實(shí)際執(zhí)行變得稍微復(fù)雜了一些。下面我們來看一下兩者的運(yùn)行機(jī)制與需要注意的問題。
基本原理
知識(shí)鋪墊
單線程模型:由于JavaScript被設(shè)計(jì)為用在瀏覽器環(huán)境,而該環(huán)境下存在大量可能發(fā)生沖突的DOM操作,為了避免進(jìn)行復(fù)雜的沖突處理(可能存在的沖突數(shù)量幾乎不可預(yù)測),JavaScript的設(shè)計(jì)者舍棄了java的多線程模型(該模型下,執(zhí)行引擎同時(shí)可以做幾件事,但要進(jìn)行線程同步),將其設(shè)計(jì)成了一門單線程語言(執(zhí)行引擎在同一時(shí)間只做一件事)。
注意:這里的單線程是指JavaScript的主線程只有一個(gè)。除了這個(gè)主線程,JavaScript還有一個(gè)I/O線程,通過事件循環(huán)來處理I/O問題,但兩者之間相對(duì)獨(dú)立,不需要進(jìn)行狀態(tài)同步,因此我們?nèi)匀豢梢园袹avaScript看成一門單線程語言。
任務(wù)隊(duì)列:所謂任務(wù)隊(duì)列,就是用于存儲(chǔ)等待執(zhí)行的任務(wù)的隊(duì)列。由于JavaScript是一門單線程語言,如果當(dāng)前有一個(gè)任務(wù)需要執(zhí)行,但JavaScript引擎正在執(zhí)行其他任務(wù),那么這個(gè)任務(wù)就需要放進(jìn)一個(gè)隊(duì)列中進(jìn)行等待。等到線程空閑時(shí),就可以從這個(gè)隊(duì)列中取出最早加入的任務(wù)進(jìn)行執(zhí)行(類似于我們?nèi)ャy行排隊(duì)辦理業(yè)務(wù)。單線程相當(dāng)于說這家銀行只有一個(gè)服務(wù)窗口,一次只能為一個(gè)人服務(wù),后面到的就需要排隊(duì),而任務(wù)隊(duì)列就是排隊(duì)區(qū),先到的就優(yōu)先服務(wù))。
注意:如果當(dāng)前線程空閑,并且隊(duì)列為空,那每次加入隊(duì)列的函數(shù)將立即執(zhí)行。
setTimeout與setInterval
setTimeout(func, delay, args) :設(shè)置超時(shí)調(diào)用。如對(duì)于setTimeout(func, 100, args),js引擎會(huì)為func函數(shù)設(shè)置一個(gè)計(jì)時(shí)器,100毫秒后,將func添加到任務(wù)隊(duì)列等待執(zhí)行。
setInterval(func, interval, args) :設(shè)置循環(huán)調(diào)用。對(duì)于語句setInterval(func, 100, args),js引擎每隔100毫秒就會(huì)把func添加到任務(wù)隊(duì)列一次。
相同點(diǎn):
兩者都會(huì)加入同一個(gè)隊(duì)列,等待線程空閑時(shí)執(zhí)行。
兩者都無法保證在何時(shí)執(zhí)行回調(diào),因?yàn)闊o法知道線程何時(shí)空閑。
不同點(diǎn)
setTimeout只會(huì)將函數(shù)添加到任務(wù)隊(duì)列一次,而setInterval則是循環(huán)往隊(duì)列中添加函數(shù)。
setTimeout可以保證函數(shù)在指定的時(shí)間間隔內(nèi)不會(huì)執(zhí)行,而setInterval無法保證(有可能出現(xiàn)接近連續(xù)執(zhí)行的情況,后面會(huì)分析原因)。
運(yùn)行機(jī)制
setTimeout
setTimeout的運(yùn)行機(jī)制相對(duì)簡單,即在執(zhí)行該語句時(shí),設(shè)置一個(gè)定時(shí)器,定時(shí)時(shí)間置為所設(shè)置的延時(shí),當(dāng)計(jì)時(shí)結(jié)束后,將傳入的函數(shù)加入任務(wù)隊(duì)列,之后的執(zhí)行就交給任務(wù)隊(duì)列負(fù)責(zé)。
setTimeout函數(shù)本身會(huì)返回一個(gè)句柄,我們可以在函數(shù)執(zhí)行前通過向clearTimeout傳入該句柄取消函數(shù)的執(zhí)行。示例代碼如下:
function func(message){ ; } //設(shè)置100毫秒后執(zhí)行func函數(shù) var timer = setTimeout(func, 100, "你好"); function cancel(){ clearTimeout(timer); //取消超時(shí)調(diào)用 }
上述代碼將在100毫秒后執(zhí)行func函數(shù),彈出一個(gè)內(nèi)容為"你好"的對(duì)話框。如果在100毫秒內(nèi)調(diào)用了cancel,就可以取消func函數(shù)的執(zhí)行。
setInterval
setInterval本質(zhì)上就是每隔一定的時(shí)間向任務(wù)隊(duì)列添加回調(diào)函數(shù)。但setInterval有一個(gè)原則:在向隊(duì)列中添加回調(diào)函數(shù)時(shí),如果隊(duì)列中存在之前由其添加的回調(diào)函數(shù),就放棄本次添加(不會(huì)影響之后的計(jì)時(shí))。另外也可以通過clearInterval方法移除定時(shí)器,使用方法同clearTimeout。
由于setInterval只負(fù)責(zé)定時(shí)向隊(duì)列中添加函數(shù),而不考慮函數(shù)的執(zhí)行,那么我們考慮一下下面的情況:
假設(shè)線程執(zhí)行完setInterval(func, 100, args)后處于完全空閑狀態(tài)(即只要向任務(wù)隊(duì)列添加函數(shù)就會(huì)立即執(zhí)行)。而func是一個(gè)相對(duì)復(fù)雜的函數(shù),執(zhí)行該函數(shù)需要90毫秒。那么函數(shù)的執(zhí)行過程就會(huì)變成下圖所示:
從圖中可以看到,從上次函數(shù)執(zhí)行完畢,到下次開始執(zhí)行,之間只間隔了10毫秒,而不是我們所希望的每隔100毫秒執(zhí)行一次(因?yàn)閟etInterval只關(guān)注任務(wù)添加,不關(guān)注任務(wù)執(zhí)行)。
由于上述機(jī)制,在很多情況下,setInterval都會(huì)遇到一些性能問題。就拿上面的例子來說,我們的本意可能是每隔100毫秒執(zhí)行一次函數(shù),結(jié)果只等待了10毫秒就又執(zhí)行了一次。另外,對(duì)于復(fù)雜的實(shí)際情況,setInterval經(jīng)常出現(xiàn)兩次的執(zhí)行間隔相差甚遠(yuǎn)的情況,對(duì)于用戶能感知到的操作,這會(huì)帶來很不好的用戶體驗(yàn)。因此在實(shí)際編碼中,開發(fā)者通常會(huì)使用setTimeout來模擬實(shí)現(xiàn)setInterval效果(下面會(huì)有舉例)。
而如果線程一開始是繁忙的,直到150毫秒處才進(jìn)入空閑狀態(tài)(假設(shè)func執(zhí)行時(shí)長為10毫秒),那么實(shí)際的運(yùn)行將變成下圖所示:
這里在100毫秒處向隊(duì)列添加func時(shí),由于線程繁忙,上次添加的func還在隊(duì)列中等待,因此直接丟棄本次要添加的函數(shù),但在200毫秒時(shí)仍然重新向隊(duì)列中添加func。
應(yīng)用場景
setTimeout
setTimeout主要用于需要進(jìn)行延時(shí)調(diào)用的場景中。如之前一篇文章介紹的js基礎(chǔ)之函數(shù)的節(jié)流與防抖,就是setTimeout典型的應(yīng)用場景。此外,由于setInterval存在的性能問題,在實(shí)際的編碼中,開發(fā)人員通常會(huì)使用setTimeout來模擬setInterval,以防止出現(xiàn)函數(shù)連續(xù)執(zhí)行的情況。如對(duì)于下面的代碼:
function func(args){ //函數(shù)本身的邏輯 ... } var timer = setInterval(func, 100, args);
我們可以通過以下代碼來實(shí)現(xiàn):
var timer; function func(args){ //函數(shù)本身的邏輯 ... //函數(shù)執(zhí)行完后,重置定時(shí)器 timer = setTimeout(func, 100, args); } timer = setTimeout(func, 100, args);
利用setTimeout保證在指定的時(shí)間內(nèi)不會(huì)執(zhí)行的特點(diǎn),我們可以在執(zhí)行完上次的回調(diào)函數(shù)后,重置定時(shí)器,實(shí)現(xiàn)循環(huán)執(zhí)行func的效果,并且從上次執(zhí)行完畢到下次執(zhí)行開始,至少會(huì)經(jīng)過100毫秒。這在實(shí)際的編碼中通常會(huì)帶來較大的性能提升,同時(shí)函數(shù)的執(zhí)行間隔也會(huì)相對(duì)穩(wěn)定。
setInterval
盡管存在上述性能問題,setInterval的使用場景相對(duì)較少,但當(dāng)所使用的接口來自外部(即回調(diào)函數(shù)本身無法修改)時(shí),就必須通過setInterval來實(shí)現(xiàn)循環(huán)執(zhí)行了。此外,對(duì)于動(dòng)畫效果來說,我們通常會(huì)希望動(dòng)畫運(yùn)行的更加平滑(也就是希望函數(shù)運(yùn)行得更頻繁),這時(shí)使用setInterval往往更加流暢,具體請(qǐng)參考之前的文章使用原生js實(shí)現(xiàn)簡單動(dòng)畫效果。
除了這類情況,開發(fā)者一般不會(huì)使用setInterval方法進(jìn)行循環(huán)調(diào)用。
補(bǔ)充說明
setTimeout與setInterval的第一個(gè)參數(shù)可以是一個(gè)匿名函數(shù),也可以是一個(gè)函數(shù)名,或者是一個(gè)字符串,如下面的寫法都是合法的:
function func(msg){ ... } //傳入回調(diào)函數(shù)名 setTimeout(func, 100, "夕山雨"); //傳入匿名函數(shù) setTimeout(function(name){ ... }, 100, "夕山雨"); //傳入字符串,js引擎會(huì)將其解析為函數(shù)體 setTimeout("", 100);
但是傳入如下的格式就可能報(bào)錯(cuò):
setTimeout(func("夕山雨"), 100);
因?yàn)檫@種寫法實(shí)際上是先調(diào)用func函數(shù),然后再將返回值添加到任務(wù)隊(duì)列。如果func的返回值不是函數(shù)(或可執(zhí)行的字符串),那么程序就會(huì)報(bào)錯(cuò);如果返回值是函數(shù),則會(huì)將返回的函數(shù)添加到任務(wù)隊(duì)列。該情況可以寫成下面的形式:
//將其作為字符串傳入,就可以被正確解析 setTimeout("func('夕山雨')", 100);
此外,當(dāng)給setTimeout傳入的延遲時(shí)間為0時(shí),并不代表回調(diào)函數(shù)會(huì)立即執(zhí)行。實(shí)際上瀏覽器規(guī)定的有一個(gè)默認(rèn)的最短計(jì)時(shí)時(shí)間,對(duì)于現(xiàn)代瀏覽器,這個(gè)時(shí)間一般為4毫秒(老版本的瀏覽器則會(huì)更長一些)。也就是說,即使傳入的延遲時(shí)間為0,瀏覽器也會(huì)至少在4毫秒后才會(huì)執(zhí)行。
上述補(bǔ)充說明同樣適用于setInterval。
總結(jié)
setTimeout與setInterval都是通過一個(gè)定時(shí)器控制回調(diào)函數(shù)的執(zhí)行,但由于javascript單線程的特點(diǎn),兩者都不能準(zhǔn)確控制函數(shù)的執(zhí)行時(shí)間點(diǎn),這點(diǎn)還請(qǐng)開發(fā)者注意。如果函數(shù)只需要執(zhí)行一次,很顯然我們會(huì)使用setTimeout來實(shí)現(xiàn);如果是循環(huán)執(zhí)行的情況,如果我們希望函數(shù)執(zhí)行頻率不那么高,并且間隔更穩(wěn)定,通常是使用setTimeout模擬實(shí)現(xiàn)setInterval效果。
總的來說,雖然都被用于函數(shù)延遲執(zhí)行,但兩者的運(yùn)行機(jī)制有本質(zhì)上的區(qū)別,所以在使用的時(shí)候請(qǐng)注意區(qū)分。
審核編輯 黃宇
-
定時(shí)器
+關(guān)注
關(guān)注
23文章
3248瀏覽量
114792 -
JS
+關(guān)注
關(guān)注
0文章
78瀏覽量
18106 -
javascript
+關(guān)注
關(guān)注
0文章
516瀏覽量
53864 -
單線程
+關(guān)注
關(guān)注
0文章
17瀏覽量
1771
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論