我們可以決定現在不執行函式,而是在稍後的特定時間執行。這稱為「排定呼叫」。
有兩種方法可以做到這一點
setTimeout
允許我們在時間間隔後執行函式一次。setInterval
允許我們重複執行函式,從時間間隔後開始,然後在該間隔內持續重複執行。
這些方法並非 JavaScript 規格的一部分。但大多數環境都有內部排程器,並提供這些方法。特別是,它們在所有瀏覽器和 Node.js 中都受支援。
setTimeout
語法
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
參數
func|code
- 要執行的函式或程式碼字串。通常是函式。基於歷史原因,可以傳遞程式碼字串,但建議不要這麼做。
delay
- 執行前的延遲時間,以毫秒為單位(1000 毫秒 = 1 秒),預設為 0。
arg1
、arg2
…- 函式的引數
例如,這段程式碼會在 1 秒後呼叫 sayHi()
function sayHi() {
alert('Hello');
}
setTimeout(sayHi, 1000);
帶有引數
function sayHi(phrase, who) {
alert( phrase + ', ' + who );
}
setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John
如果第一個引數是字串,JavaScript 會從中建立函式。
因此,這段程式碼也會執行
setTimeout("alert('Hello')", 1000);
但建議不要使用字串,改用箭頭函式,如下所示
setTimeout(() => alert('Hello'), 1000);
新手開發人員有時會在函式後加上括號 ()
,這是一個錯誤
// wrong!
setTimeout(sayHi(), 1000);
這不會執行,因為 setTimeout
預期的是函式參考。而這裡的 sayHi()
會執行函式,而其執行結果會傳遞給 setTimeout
。在我們的案例中,sayHi()
的結果是 undefined
(函式沒有傳回任何東西),因此不會排程任何事情。
使用 clearTimeout 取消
呼叫 setTimeout
會傳回「計時器識別碼」timerId
,我們可以使用它來取消執行。
取消語法
let timerId = setTimeout(...);
clearTimeout(timerId);
在以下程式碼中,我們排程函式,然後取消它(改變心意)。因此,不會發生任何事情
let timerId = setTimeout(() => alert("never happens"), 1000);
alert(timerId); // timer identifier
clearTimeout(timerId);
alert(timerId); // same identifier (doesn't become null after canceling)
從 alert
輸出中可以看到,在瀏覽器中,計時器識別碼是一個數字。在其他環境中,這可能是其他東西。例如,Node.js 會傳回具有其他方法的計時器物件。
同樣地,這些方法沒有通用規格,因此沒關係。
對於瀏覽器,計時器在 HTML Living Standard 的計時器區段中說明。
setInterval
setInterval
方法的語法與 setTimeout
相同
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
所有引數的意義都相同。但與 setTimeout
不同的是,它不僅會執行函式一次,還會在給定的時間間隔後定期執行。
若要停止後續呼叫,我們應該呼叫 clearInterval(timerId)
。
以下範例會每 2 秒顯示訊息。5 秒後,輸出會停止
// repeat with the interval of 2 seconds
let timerId = setInterval(() => alert('tick'), 2000);
// after 5 seconds stop
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);
alert
時,時間會繼續進行在包括 Chrome 和 Firefox 在內的大多數瀏覽器中,內部計時器在顯示 alert/confirm/prompt
時會持續「滴答作響」。
因此,如果您執行上述程式碼,並且一段時間內不關閉 alert
視窗,那麼當您關閉時,下一個 alert
會立即顯示。警示之間的實際間隔將短於 2 秒。
巢狀 setTimeout
有兩種方式可以定期執行某件事。
一種是 setInterval
。另一種是巢狀 setTimeout
,如下所示
/** instead of:
let timerId = setInterval(() => alert('tick'), 2000);
*/
let timerId = setTimeout(function tick() {
alert('tick');
timerId = setTimeout(tick, 2000); // (*)
}, 2000);
上述的 setTimeout
會在目前呼叫的最後 (*)
安排下一次呼叫。
巢狀 setTimeout
是一種比 setInterval
更靈活的方法。這樣一來,下一次呼叫可以根據目前呼叫的結果安排成不同時間。
例如,我們需要寫一個服務,每 5 秒向伺服器傳送一個請求以取得資料,但如果伺服器過載,則應將間隔時間增加到 10、20、40 秒……
以下是偽程式碼
let delay = 5000;
let timerId = setTimeout(function request() {
...send request...
if (request failed due to server overload) {
// increase the interval to the next run
delay *= 2;
}
timerId = setTimeout(request, delay);
}, delay);
如果我們安排的函式會大量使用 CPU,則可以測量執行時間,並提早或延後安排下一次呼叫。
巢狀 setTimeout
允許將執行之間的延遲設定得比 setInterval
更精確。
我們來比較兩個程式碼片段。第一個使用 setInterval
let i = 1;
setInterval(function() {
func(i++);
}, 100);
第二個使用巢狀 setTimeout
let i = 1;
setTimeout(function run() {
func(i++);
setTimeout(run, 100);
}, 100);
對於 setInterval
,內部排程器會每 100 毫秒執行 func(i++)
你注意到了嗎?
對於 setInterval
,func
呼叫之間的實際延遲小於程式碼中的延遲!
這是正常的,因為 func
執行的時間會「消耗」一部分的間隔時間。
有可能 func
的執行時間比我們預期的長,並超過 100 毫秒。
在這種情況下,引擎會等到 func
完成,然後檢查排程器,如果時間到了,就會立即再次執行。
在極端情況下,如果函式總是執行超過 delay
毫秒,則呼叫將會完全沒有暫停地發生。
以下是巢狀 setTimeout
的圖示
巢狀 setTimeout
保證固定的延遲(在此為 100 毫秒)。
這是因為會在前一次呼叫結束時安排新的呼叫。
當函式傳遞到 setInterval/setTimeout
時,會建立一個內部參考並儲存在排程器中。即使沒有其他參考,它也會防止函式被垃圾回收。
// the function stays in memory until the scheduler calls it
setTimeout(function() {...}, 100);
對於 setInterval
,函式會保留在記憶體中,直到呼叫 clearInterval
為止。
有一個副作用。函式會參照外部詞彙環境,因此,只要函式存在,外部變數也會存在。它們可能比函式本身佔用更多記憶體。因此,當我們不再需要排程函式時,最好取消它,即使它很小。
零延遲 setTimeout
有一個特殊的使用案例:setTimeout(func, 0)
,或僅為 setTimeout(func)
。
這會盡快排程執行 func
。但排程器只會在目前執行的指令碼完成後呼叫它。
因此,函式會排程在目前指令碼「之後」執行。
例如,這會輸出「Hello」,然後立即輸出「World」
setTimeout(() => alert("World"));
alert("Hello");
第一行「在 0 毫秒後將呼叫放入行事曆」。但排程器只會在目前指令碼完成後「檢查行事曆」,因此 "Hello"
會先輸出,"World"
會在它之後輸出。
還有進階的與瀏覽器相關的零延遲逾時使用案例,我們會在章節 事件迴圈:微任務和巨集任務 中討論。
在瀏覽器中,巢狀計時器可以執行的頻率有限制。HTML Living Standard 指出:「在五個巢狀計時器之後,間隔會強制設定為至少 4 毫秒。」
讓我們透過以下範例來說明它的意思。其中的 setTimeout
呼叫會以零延遲重新排程它自己。每個呼叫會在 times
陣列中記住前一個呼叫的實際時間。實際延遲是什麼?讓我們看看
let start = Date.now();
let times = [];
setTimeout(function run() {
times.push(Date.now() - start); // remember delay from the previous call
if (start + 100 < Date.now()) alert(times); // show the delays after 100ms
else setTimeout(run); // else re-schedule
});
// an example of the output:
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
第一個計時器會立即執行(正如規範中所寫),然後我們會看到 9, 15, 20, 24...
。呼叫之間強制性的 4+ 毫秒延遲會發揮作用。
如果我們使用 setInterval
取代 setTimeout
,也會發生類似的事情:setInterval(f)
會以零延遲執行 f
幾次,然後再以 4+ 毫秒延遲執行。
這種限制來自於遠古時代,許多指令碼依賴於它,因此它存在於歷史原因。
對於伺服器端 JavaScript,這種限制不存在,而且還有其他方法可以排程立即非同步工作,例如 Node.js 的 setImmediate。因此,此註解是瀏覽器專用的。
摘要
- 方法
setTimeout(func, delay, ...args)
和setInterval(func, delay, ...args)
讓我們可以在delay
毫秒後執行func
一次/定期。 - 若要取消執行,我們應該使用
setTimeout/setInterval
傳回的值呼叫clearTimeout/clearInterval
。 - 巢狀
setTimeout
呼叫是setInterval
更靈活的替代方案,讓我們可以更精確地設定執行之間的時間。 - 使用
setTimeout(func, 0)
(與setTimeout(func)
相同)進行零延遲排程,用於排程「盡快呼叫,但在目前指令碼完成後」。 - 瀏覽器會將五次或更多次巢狀呼叫
setTimeout
或setInterval
(在第五次呼叫後)的最小延遲限制為 4 毫秒。這是基於歷史原因。
請注意,所有排程方法都不會保證確切的延遲。
例如,瀏覽器計時器可能會因為很多原因而變慢
- CPU 過載。
- 瀏覽器標籤處於背景模式。
- 筆電處於省電模式。
所有這些都可能會將最小計時器解析度(最小延遲)增加到 300 毫秒,甚至 1000 毫秒,具體取決於瀏覽器和作業系統層級效能設定。
留言
<code>
標籤,要插入多行程式碼,請將它們包在<pre>
標籤中,要插入 10 行以上的程式碼,請使用沙盒 (plnkr、jsbin、codepen…)