W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
有時(shí)我們并不想立即執(zhí)行一個(gè)函數(shù),而是等待特定一段時(shí)間之后再執(zhí)行。這就是所謂的“計(jì)劃調(diào)用(scheduling a call)”。
目前有兩種方式可以實(shí)現(xiàn):
setTimeout
? 允許我們將函數(shù)推遲到一段時(shí)間間隔之后再執(zhí)行。setInterval
? 允許我們重復(fù)運(yùn)行一個(gè)函數(shù),從一段時(shí)間間隔之后開始運(yùn)行,之后以該時(shí)間間隔連續(xù)重復(fù)運(yùn)行該函數(shù)。這兩個(gè)方法并不在 JavaScript 的規(guī)范中。但是大多數(shù)運(yùn)行環(huán)境都有內(nèi)建的調(diào)度程序,并且提供了這些方法。目前來講,所有瀏覽器以及 Node.js 都支持這兩個(gè)方法。
語法:
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
參數(shù)說明:
?func|code
?
想要執(zhí)行的函數(shù)或代碼字符串。 一般傳入的都是函數(shù)。由于某些歷史原因,支持傳入代碼字符串,但是不建議這樣做。
?delay
?
執(zhí)行前的延時(shí),以毫秒為單位(1000 毫秒 = 1 秒),默認(rèn)值是 0;
?arg1,arg2…
?
要傳入被執(zhí)行函數(shù)(或代碼字符串)的參數(shù)列表(IE9 以下不支持)
例如,在下面這個(gè)示例中,?sayHi()
? 方法會(huì)在 1 秒后執(zhí)行:
function sayHi() {
alert('Hello');
}
setTimeout(sayHi, 1000);
帶參數(shù)的情況:
function sayHi(phrase, who) {
alert( phrase + ', ' + who );
}
setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John
如果第一個(gè)參數(shù)位傳入的是字符串,JavaScript 會(huì)自動(dòng)為其創(chuàng)建一個(gè)函數(shù)。
所以這么寫也是可以的:
setTimeout("alert('Hello')", 1000);
但是,不建議使用字符串,我們可以使用箭頭函數(shù)代替它們,如下所示:
setTimeout(() => alert('Hello'), 1000);
傳入一個(gè)函數(shù),但不要執(zhí)行它
新手開發(fā)者有時(shí)候會(huì)誤將一對括號
()
加在函數(shù)后面:
// 錯(cuò)的! setTimeout(sayHi(), 1000);
這樣不行,因?yàn)?nbsp;
setTimeout
期望得到一個(gè)對函數(shù)的引用。而這里的sayHi()
很明顯是在執(zhí)行函數(shù),所以實(shí)際上傳入setTimeout
的是 函數(shù)的執(zhí)行結(jié)果。在這個(gè)例子中,sayHi()
的執(zhí)行結(jié)果是undefined
(也就是說函數(shù)沒有返回任何結(jié)果),所以實(shí)際上什么也沒有調(diào)度。
setTimeout
在調(diào)用時(shí)會(huì)返回一個(gè)“定時(shí)器標(biāo)識符(timer identifier)”,在我們的例子中是 timerId
,我們可以使用它來取消執(zhí)行。
取消調(diào)度的語法:
let timerId = setTimeout(...);
clearTimeout(timerId);
在下面的代碼中,我們對一個(gè)函數(shù)進(jìn)行了調(diào)度,緊接著取消了這次調(diào)度(中途反悔了)。所以最后什么也沒發(fā)生:
let timerId = setTimeout(() => alert("never happens"), 1000);
alert(timerId); // 定時(shí)器標(biāo)識符
clearTimeout(timerId);
alert(timerId); // 還是這個(gè)標(biāo)識符(并沒有因?yàn)檎{(diào)度被取消了而變成 null)
從 alert
的輸出來看,在瀏覽器中,定時(shí)器標(biāo)識符是一個(gè)數(shù)字。在其他環(huán)境中,可能是其他的東西。例如 Node.js 返回的是一個(gè)定時(shí)器對象,這個(gè)對象包含一系列方法。
我再重申一遍,這些方法沒有統(tǒng)一的規(guī)范定義,所以這沒什么問題。
針對瀏覽器環(huán)境,定時(shí)器在 HTML5 的標(biāo)準(zhǔn)中有詳細(xì)描述,詳見 timers section。
setInterval
方法和 setTimeout
的語法相同:
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
所有參數(shù)的意義也是相同的。不過與 setTimeout
只執(zhí)行一次不同,setInterval
是每間隔給定的時(shí)間周期性執(zhí)行。
想要阻止后續(xù)調(diào)用,我們需要調(diào)用 clearInterval(timerId)
。
下面的例子將每間隔 2 秒就會(huì)輸出一條消息。5 秒之后,輸出停止:
// 每 2 秒重復(fù)一次
let timerId = setInterval(() => alert('tick'), 2000);
// 5 秒之后停止
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);
alert 彈窗顯示的時(shí)候計(jì)時(shí)器依然在進(jìn)行計(jì)時(shí)
在大多數(shù)瀏覽器中,包括 Chrome 和 Firefox,在顯示
alert/confirm/prompt
彈窗時(shí),內(nèi)部的定時(shí)器仍舊會(huì)繼續(xù)“嘀嗒”。
所以,在運(yùn)行上面的代碼時(shí),如果在一定時(shí)間內(nèi)沒有關(guān)掉
alert
彈窗,那么在你關(guān)閉彈窗后,下一個(gè)alert
會(huì)立即顯示。兩次alert
之間的時(shí)間間隔將小于 2 秒。
周期性調(diào)度有兩種方式。
一種是使用 ?setInterval
?,另外一種就是嵌套的 ?setTimeout
?,就像這樣:
/** instead of:
let timerId = setInterval(() => alert('tick'), 2000);
*/
let timerId = setTimeout(function tick() {
alert('tick');
timerId = setTimeout(tick, 2000); // (*)
}, 2000);
上面這個(gè) setTimeout
在當(dāng)前這一次函數(shù)執(zhí)行完時(shí) (*)
立即調(diào)度下一次調(diào)用。
嵌套的 setTimeout
要比 setInterval
靈活得多。采用這種方式可以根據(jù)當(dāng)前執(zhí)行結(jié)果來調(diào)度下一次調(diào)用,因此下一次調(diào)用可以與當(dāng)前這一次不同。
例如,我們要實(shí)現(xiàn)一個(gè)服務(wù)(server),每間隔 5 秒向服務(wù)器發(fā)送一個(gè)數(shù)據(jù)請求,但如果服務(wù)器過載了,那么就要降低請求頻率,比如將間隔增加到 10、20、40 秒等。
以下是偽代碼:
let delay = 5000;
let timerId = setTimeout(function request() {
...發(fā)送請求...
if (request failed due to server overload) {
// 下一次執(zhí)行的間隔是當(dāng)前的 2 倍
delay *= 2;
}
timerId = setTimeout(request, delay);
}, delay);
并且,如果我們調(diào)度的函數(shù)占用大量的 CPU,那么我們可以測量執(zhí)行所需要花費(fèi)的時(shí)間,并安排下次調(diào)用是應(yīng)該提前還是推遲。
嵌套的 setTimeout
相較于 setInterval
能夠更精確地設(shè)置兩次執(zhí)行之間的延時(shí)。
下面來比較這兩個(gè)代碼片段。第一個(gè)使用的是 setInterval
:
let i = 1;
setInterval(function() {
func(i++);
}, 100);
第二個(gè)使用的是嵌套的 setTimeout
:
let i = 1;
setTimeout(function run() {
func(i++);
setTimeout(run, 100);
}, 100);
對 setInterval
而言,內(nèi)部的調(diào)度程序會(huì)每間隔 100 毫秒執(zhí)行一次 func(i++)
:
注意到了嗎?
使用 setInterval
時(shí),func
函數(shù)的實(shí)際調(diào)用間隔要比代碼中設(shè)定的時(shí)間間隔要短!
這也是正常的,因?yàn)?nbsp;func
的執(zhí)行所花費(fèi)的時(shí)間“消耗”了一部分間隔時(shí)間。
也可能出現(xiàn)這種情況,就是 func
的執(zhí)行所花費(fèi)的時(shí)間比我們預(yù)期的時(shí)間更長,并且超出了 100 毫秒。
在這種情況下,JavaScript 引擎會(huì)等待 func
執(zhí)行完成,然后檢查調(diào)度程序,如果時(shí)間到了,則 立即 再次執(zhí)行它。
極端情況下,如果函數(shù)每次執(zhí)行時(shí)間都超過 delay
設(shè)置的時(shí)間,那么每次調(diào)用之間將完全沒有停頓。
這是嵌套的 setTimeout
的示意圖:
嵌套的 setTimeout
就能確保延時(shí)的固定(這里是 100 毫秒)。
這是因?yàn)橄乱淮握{(diào)用是在前一次調(diào)用完成時(shí)再調(diào)度的。
垃圾回收和 setInterval/setTimeout 回調(diào)(callback)
當(dāng)一個(gè)函數(shù)傳入
setInterval/setTimeout
時(shí),將為其創(chuàng)建一個(gè)內(nèi)部引用,并保存在調(diào)度程序中。這樣,即使這個(gè)函數(shù)沒有其他引用,也能防止垃圾回收器(GC)將其回收。
// 在調(diào)度程序調(diào)用這個(gè)函數(shù)之前,這個(gè)函數(shù)將一直存在于內(nèi)存中 setTimeout(function() {...}, 100);
對于
setInterval
,傳入的函數(shù)也是一直存在于內(nèi)存中,直到clearInterval
被調(diào)用。
這里還要提到一個(gè)副作用。如果函數(shù)引用了外部變量(譯注:閉包),那么只要這個(gè)函數(shù)還存在,外部變量也會(huì)隨之存在。它們可能比函數(shù)本身占用更多的內(nèi)存。因此,當(dāng)我們不再需要調(diào)度函數(shù)時(shí),最好取消它,即使這是個(gè)(占用內(nèi)存)很小的函數(shù)。
這兒有一種特殊的用法:setTimeout(func, 0)
,或者僅僅是 setTimeout(func)
。
這樣調(diào)度可以讓 func
盡快執(zhí)行。但是只有在當(dāng)前正在執(zhí)行的腳本執(zhí)行完成后,調(diào)度程序才會(huì)調(diào)用它。
也就是說,該函數(shù)被調(diào)度在當(dāng)前腳本執(zhí)行完成“之后”立即執(zhí)行。
例如,下面這段代碼會(huì)先輸出 “Hello”,然后立即輸出 “World”:
setTimeout(() => alert("World"));
alert("Hello");
第一行代碼“將調(diào)用安排到日程(calendar)0 毫秒處”。但是調(diào)度程序只有在當(dāng)前腳本執(zhí)行完畢時(shí)才會(huì)去“檢查日程”,所以先輸出 "Hello"
,然后才輸出 "World"
。
此外,還有與瀏覽器相關(guān)的 0 延時(shí) timeout 的高級用例,我們將在 事件循環(huán):微任務(wù)和宏任務(wù) 一章中詳細(xì)講解。
零延時(shí)實(shí)際上不為零(在瀏覽器中)
在瀏覽器環(huán)境下,嵌套定時(shí)器的運(yùn)行頻率是受限制的。根據(jù) HTML5 標(biāo)準(zhǔn) 所講:“經(jīng)過 5 重嵌套定時(shí)器之后,時(shí)間間隔被強(qiáng)制設(shè)定為至少 4 毫秒”。
讓我們用下面的示例來看看這到底是什么意思。其中
setTimeout
調(diào)用會(huì)以零延時(shí)重新調(diào)度自身的調(diào)用。每次調(diào)用都會(huì)在times
數(shù)組中記錄上一次調(diào)用的實(shí)際時(shí)間。那么真正的延遲是什么樣的?讓我們來看看:
let start = Date.now(); let times = []; setTimeout(function run() { times.push(Date.now() - start); // 保存前一個(gè)調(diào)用的延時(shí) if (start + 100 < Date.now()) alert(times); // 100 毫秒之后,顯示延時(shí)信息 else setTimeout(run); // 否則重新調(diào)度 }); // 輸出示例: // 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
第一次,定時(shí)器是立即執(zhí)行的(正如規(guī)范里所描述的那樣),接下來我們可以看到
9, 15, 20, 24...
。兩次調(diào)用之間必須經(jīng)過 4 毫秒以上的強(qiáng)制延時(shí)。(譯注:這里作者沒說清楚,timer 數(shù)組里存放的是每次定時(shí)器運(yùn)行的時(shí)刻與 start 的差值,所以數(shù)字只會(huì)越來越大,實(shí)際上前后調(diào)用的延時(shí)是數(shù)組值的差值。示例中前幾次都是 1,所以延時(shí)為 0)
如果我們使用
setInterval
而不是setTimeout
,也會(huì)發(fā)生類似的情況:setInterval(f)
會(huì)以零延時(shí)運(yùn)行幾次f
,然后以 4 毫秒以上的強(qiáng)制延時(shí)運(yùn)行。
這個(gè)限制來自“遠(yuǎn)古時(shí)代”,并且許多腳本都依賴于此,所以這個(gè)機(jī)制也就存在至今。
對于服務(wù)端的 JavaScript,就沒有這個(gè)限制,并且還有其他調(diào)度即時(shí)異步任務(wù)的方式。例如 Node.js 的 setImmediate。因此,這個(gè)提醒只是針對瀏覽器環(huán)境的。
setTimeout(func, delay, ...args)
? 和 ?setInterval(func, delay, ...args)
? 方法允許我們在 ?delay
? 毫秒之后運(yùn)行 ?func
? 一次或以 ?delay
? 毫秒為時(shí)間間隔周期性運(yùn)行 ?func
?。clearInterval/clearTimeout
?,并將 ?setInterval/setTimeout
? 返回的值作為入?yún)魅搿?/li>
setTimeout
? 比 ?setInterval
? 用起來更加靈活,允許我們更精確地設(shè)置兩次執(zhí)行之間的時(shí)間。setTimeout(func, 0)
?(與 ?setTimeout(func)
? 相同)用來調(diào)度需要盡快執(zhí)行的調(diào)用,但是會(huì)在當(dāng)前腳本執(zhí)行完成后進(jìn)行調(diào)用。setTimeout
? 或 ?setInterval
? 的五層或更多層嵌套調(diào)用(調(diào)用五次之后)的最小延時(shí)限制在 4ms。這是歷史遺留問題。請注意,所有的調(diào)度方法都不能 保證 確切的延時(shí)。
例如,瀏覽器內(nèi)的計(jì)時(shí)器可能由于許多原因而變慢:
所有這些因素,可能會(huì)將定時(shí)器的最小計(jì)時(shí)器分辨率(最小延遲)增加到 300ms 甚至 1000ms,具體以瀏覽器及其設(shè)置為準(zhǔn)。
編寫一個(gè)函數(shù) ?printNumbers(from, to)
?,使其每秒輸出一個(gè)數(shù)字,數(shù)字從 ?from
? 開始,到 ?to
? 結(jié)束。
使用以下兩種方法來實(shí)現(xiàn)。
setInterval
?。setTimeout
?。使用 setInterval
:
function printNumbers(from, to) {
let current = from;
let timerId = setInterval(function() {
alert(current);
if (current == to) {
clearInterval(timerId);
}
current++;
}, 1000);
}
// 用例:
printNumbers(5, 10);
使用嵌套的 setTimeout
:
function printNumbers(from, to) {
let current = from;
setTimeout(function go() {
alert(current);
if (current < to) {
setTimeout(go, 1000);
}
current++;
}, 1000);
}
// 用例:
printNumbers(5, 10);
請注意,在這兩種解決方案中,在第一個(gè)輸出之前都有一個(gè)初始延遲。函數(shù)在 1000ms
之后才被第一次調(diào)用。
如果我們還希望函數(shù)立即運(yùn)行,那么我們可以在單獨(dú)的一行上添加一個(gè)額外的調(diào)用,像這樣:
function printNumbers(from, to) {
let current = from;
function go() {
alert(current);
if (current == to) {
clearInterval(timerId);
}
current++;
}
go();
let timerId = setInterval(go, 1000);
}
printNumbers(5, 10);
下面代碼中使用 setTimeout
調(diào)度了一個(gè)調(diào)用,然后需要運(yùn)行一個(gè)計(jì)算量很大的 for
循環(huán),這段運(yùn)算耗時(shí)超過 100 毫秒。
調(diào)度的函數(shù)會(huì)在何時(shí)運(yùn)行?
?alert
? 會(huì)顯示什么?
let i = 0;
setTimeout(() => alert(i), 100); // ?
// 假設(shè)這段代碼的運(yùn)行時(shí)間 >100ms
for(let j = 0; j < 100000000; j++) {
i++;
}
任何 ?setTimeout
? 都只會(huì)在當(dāng)前代碼執(zhí)行完畢之后才會(huì)執(zhí)行。
所以 ?i
? 的取值為:?100000000
?。
let i = 0;
setTimeout(() => alert(i), 100); // 100000000
// 假設(shè)這段代碼的運(yùn)行時(shí)間 >100ms
for(let j = 0; j < 100000000; j++) {
i++;
}
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: