JavaScript 定時器
1.導讀
在寫 setTimeout
和 setInterval
程式碼時,你是否有想過一下幾點:
- 他們是怎麼實現的?
- 面試時如果問你原理怎麼回答?
- 為什麼要了解定時器原理?
首先 setTimeout
和 setInterval
都不是ECMAScript
規範或者任何JavaScript
實現的一部分。它是由瀏覽器實現,並且在不同的瀏覽器也會有所差異。定時器也可以由 Nodejs
執行時本身實現。
在瀏覽器中,定時器是 Window
物件下的 api,所以可以直接在控制檯進行直接呼叫。
在 Nodejs
中,定時器是 global
物件的一部分,這點和瀏覽器的 Window
類似。具體可以去檢視下node-timers原始碼
有些人肯定會想,為什麼一定要了解這些糟糕無聊的原理,我們只需要運用別人 api 進行開發不就可以了。很遺憾的告訴你,作為一名 JavaScript
開發人員,我認為如果你只是想一直做一個初級開發工程師,那麼你可以不去了解,如果想要提升,如果不去了解,那可能表明你並不完全理解V8(和其他虛擬機器)如何與瀏覽器和Node互動。
本文會通過案例來講解 JavaScript 定時器,還會講解某條的一些面試題
2.定時器的一些案例
2.1 延遲案例
// eg1.js
setTimeout(
() => {
console.log('Hello after 4 seconds');
},
4 * 1000
);
複製程式碼
上面這個例子用 setTimeout
延時 4 秒列印問候語。
如果你在node環境執行 example1.js。Node將會暫停4秒然後列印問候語(接著退出)。
setTimeout
第一個引數function - 是你想要在到期時間(delay毫秒)之後執行的函式。
【注意:】 setTimeout
的第一個引數只是一個函式引用。 它不必像eg1.js
那樣是行內函數。 這是不使用行內函數的相同示例:
const func = () => {
console.log('Hello after 4 seconds');
};
setTimeout(func, 4 * 1000);
複製程式碼
setTimeout
第二個引數 delay - 延遲的毫秒數 (一秒等於1000毫秒),函式的呼叫會在該延遲之後發生。如果省略該引數,delay取預設值0,意味著“馬上”執行,或者儘快執行。不管是哪種情況,實際的延遲時間可能會比期待的(delay毫秒數) 值長setTimeout
第三個引數 param1, ..., paramN 可選 附加引數,一旦定時器到期,它們會作為引數傳遞給 function
/ For: func(arg1, arg2, arg3, ...)
// We can use: setTimeout(func, delay, arg1, arg2, arg3, ...)
複製程式碼
具體例項如下:
// example2.js
const rocks = who => {
console.log(who + ' rocks');
};
setTimeout(rocks, 2 * 1000, 'Node.js');
複製程式碼
上面的rocks
延遲2秒執行,接收who
引數並且通過setTimeout
中轉字串 “Node.js” 給函式的who
引數。
在 node 環境執行 example2.js 控制檯會在2秒後列印 “Node.js rocks”
2.2 案例2
使用您到目前為止學到的關於setTimeout
的知識,在相應的延遲後列印以下 2 條訊息。
-
4 秒後列印訊息 “Hello after 4 seconds”
-
8 秒後列印 “Hello after 8 seconds” 訊息。
【注意:】您只能在解決方案中定義一個函式,其中包括行內函數。 這意味著許多 setTimeout
呼叫必須使用完全相同的函式。
我們應該會很快寫出如下程式碼:
// solution1.js
const theOneFunc = delay => {
console.log('Hello after ' + delay + ' seconds');
};
setTimeout(theOneFunc, 4 * 1000, 4);
setTimeout(theOneFunc, 8 * 1000, 8);
複製程式碼
theOneFunc
收到一個delay
引數,並在列印的訊息中使用了delay
引數的值。 這樣,該函式可以根據我們傳遞給它的任何延遲值列印不同的訊息。
然後在兩次setTimeout
的呼叫中使用了theOneFunc
,一個在 4 秒後觸發,另一個在 8 秒後觸發。 這兩個setTimeout
呼叫也得到一個 第三個 引數來表示theOneFunc的delay
引數。
使用 node
命令執行 solution1.js
檔案將列印出挑戰要求的內容,4 秒後的第一條訊息和 8 秒後的第二條訊息。
2.3 setInterval 案例
如果要求你每隔 4秒 列印一條訊息怎麼辦?
雖然你可以將setTimeout
放在一個迴圈中,但定時器API
也提供了setInterval
函式,這將完成永遠做某事的要求。
// example3.js
setInterval(
() => console.log('Hello every 4 seconds'),
4000
);
複製程式碼
此示例將每4秒列印一次訊息。 使用 node 命令執行 example3.js 將使 Node 永遠列印此訊息,直到你終止該程式.
2.4 清除定時器
對setTimeout
的呼叫返回一個定時器“ID”,你可以使用帶有clearTimeout
呼叫的定時器ID來取消該定時器。 下面是這個例子:
// example4.js
const timerId = setTimeout(
() => console.log('You will not see this one!'),
0
);
clearTimeout(timerId);
複製程式碼
這個簡單的計時器應該在“0”ms之後觸發(使其立即生效),但它不會因為我們正在捕獲timerId
值並在使用clearTimeout
呼叫後立即取消它。
當我們用 node 命令執行 example4.js 時,Node 不會列印任何東西,程式就會退出。
順便說一句,在 Node.js 中,還有另一種方法可以使用0 ms來執行setTimeout
。 Node.js 計時器API有另一個名為setImmediate
的函式,它與setTimeout
基本相同,帶有0 ms但我們不必在那裡指定延遲:
setImmediate(
() => console.log('I am equivalent to setTimeout with 0 ms'),
);
複製程式碼
setImmediate
方法在所有瀏覽器裡都不支援。不要在前端程式碼裡使用它。
就像clearTimeout
一樣,還有一個clearInterval
函式,它對於setInerval
呼叫執行相同的操作,並且還有一個clearImmediate
呼叫。
在前面的例子中,您是否注意到在“0”ms之後執行帶有setTimeout
的內容並不意味著立即執行它(在setTimeout
行之後),而是在指令碼中的所有其他內容之後立即執行它(包括clearTimeout
呼叫)?
讓我用一個例子清楚地說明這一點。 這是一個簡單的setTimeout
呼叫,應該在半秒後觸發,但它不會:
// example5.js
setTimeout(
() => console.log('Hello after 0.5 seconds. MAYBE!'),
500,
);
for (let i = 0; i < 1e10; i++) {
// Block Things Synchronously
}
複製程式碼
在此示例中定義計時器之後,我們使用大的for迴圈同步阻止執行時。 1e10是1後面有10個零,所以迴圈是一個10個十億滴答迴圈(基本上模擬繁忙的CPU)。 當此迴圈正在滴答時,節點無法執行任何操作。
實踐中做的非常糟糕的事情,但它會幫助你理解setTimeout
延遲不是一個保證的東西,而是一個最小的東西。 500ms表示最小延遲為500ms。 實際上,指令碼將花費更長的時間來列印其問候語。 它必須等待阻塞迴圈才能完成。
推薦大家看一篇Node.js Event loop 原理 裡面講的很深。
2.4 列印指令碼並推出程式
編寫指令碼每秒列印訊息“ Hello World ”,但只列印5次。 5次之後,指令碼應該列印訊息“Done”並讓節點程式退出。
【注意:】你不能使用setTimeout呼叫來完成這個挑戰。 提示:你需要一個計數器。
let counter = 0;
const intervalId = setInterval(() => {
console.log('Hello World');
counter += 1;
if (counter === 5) {
console.log('Done');
clearInterval(intervalId);
}
}, 1000);
複製程式碼
counter 值作為 0 啟動,然後啟動一個 setInterval 呼叫同時捕獲它的id。
延遲功能將列印訊息並每次遞增計數器。 在延遲函式內部,if語句將檢查我們現在是否處於5次。 如果是這樣,它將列印“Done”並使用捕獲的 intervalId 常量清除間隔。 間隔延遲為“1000”ms。
2.5 this 和定時器結合時
當你在常規函式中使用JavaScript的this關鍵字時,如下所示:
function whoCalledMe() {
console.log('Caller is', this);
}
複製程式碼
this 關鍵字內的值將代表函式的呼叫者。 如果在 Node REPL 中定義上面的函式,則呼叫者將是 global 物件。 如果在瀏覽器的控制檯中定義函式,則呼叫者將是 window 物件。
讓我們將函式定義為物件的屬性,以使其更清晰:
const obj = {
id: '42',
whoCalledMe() {
console.log('Caller is', this);
}
};
// The function reference is now: obj.whoCallMe
複製程式碼
現在當你直接使用它的引用呼叫 obj.whoCallMe 函式時,呼叫者將是 obj 物件(由其id標識)
現在,問題是,如果我們將 obj.whoCallMe 的引用傳遞給 setTimetout 呼叫,呼叫者會是什麼?
// What will this print??
setTimeout(obj.whoCalledMe, 0);
複製程式碼
在這種情況下呼叫者會是誰?
答案根據執行計時器功能的位置而有所不同。 在這種情況下,你根本無法取決於呼叫者是誰。 你失去了對呼叫者的控制權,因為定時器實現將是現在呼叫您的函式的實現。 如果你在Node REPL中測試它,你會得到一個 Timetout 物件作為呼叫者
【注意】這隻在您在常規函式中使用JavaScript的this關鍵字時才有意義。 如果您使用箭頭函式,則根本不需要擔心呼叫者。
2.6 連續列印具有不同延遲的訊息“Hello World”
以1秒的延遲開始,然後每次將延遲增加1秒。 第二次將延遲2秒。 第三次將延遲3秒,依此類推。
在列印的訊息中包含延遲時間。 預期輸出看起來像:
Hello World. 1
Hello World. 2
Hello World. 3...
複製程式碼
【注意】你只能使用const來定義變數。 你不能使用 let 或 var。 我們先進行分析如下:
- 因為延遲量是這個挑戰中的一個變數,我們不能在這裡使用
setInterval
,但我們可以在遞迴呼叫中使用setTimeout
手動建立一個間隔執行。 使用setTimeout
的第一個執行函式將建立另一個計時器,依此類推。 - 另外,因為我們不能使用let / var,所以我們不能有一個計數器來增加每個遞迴呼叫的延遲時間,但我們可以使用遞迴函式引數在遞迴呼叫期間遞增。
以下是解決問題的一種方法:
const greeting = delay =>
setTimeout(() => {
console.log('Hello World. ' + delay);
greeting(delay + 1);
}, delay * 1000);
greeting(1);
複製程式碼
編寫一個指令碼以連續列印訊息“Hello World”,其具有與挑戰#3相同的變化延遲概念,但這次是每個主延遲間隔的 5個訊息組。 從前5個訊息的延遲 100ms 開始,接下來的5個訊息延遲 200ms,然後是 300ms,依此類推。
以下是程式碼的要求:
-
在100ms點,指令碼將開始列印“Hello World”,並以100ms的間隔進行5次。 第一條訊息將出現在100毫秒,第二條訊息將出現在200毫秒,依此類推。
-
在前5條訊息之後,指令碼應將主延遲增加到200ms。 因此,第6條訊息將在500毫秒+ 200毫秒(700毫秒)列印,第7條訊息將在900毫秒列印,第8條訊息將在1100毫秒列印,依此類推。
-
在10條訊息之後,指令碼應將主延遲增加到300毫秒。 所以第11條訊息應該在500ms + 1000ms + 300ms(18000ms)列印。 第12條訊息應列印在21000ms,依此類推。
一直重複上面的模式。
Hello World. 100 // At 100ms
Hello World. 100 // At 200ms
Hello World. 100 // At 300ms
Hello World. 100 // At 400ms
Hello World. 100 // At 500ms
Hello World. 200 // At 700ms
Hello World. 200 // At 900ms
Hello World. 200 // At 1100ms...
複製程式碼
【注意】您只能使用 setInterval 呼叫(而不是 setTimeout),並且只能使用一個 if 語句。
以下是一種解決辦法
let lastIntervalId, counter = 5;
const greeting = delay => {
if (counter === 5) {
clearInterval(lastIntervalId);
lastIntervalId = setInterval(() => {
console.log('Hello World. ', delay);
greeting(delay + 100);
}, delay);
counter = 0;
}
counter += 1;
};
greeting(100);
複製程式碼
3.面試中的定時器
3.1 某條 - 使用 JS 實現一個 repeat 方法
使用 JS 實現一個 repeat 方法,輸入輸出如下:
// 實現
function repeat (func, times, wait) {},
// 輸入
const repeatFunc = repeat(alert, 4, 3000);
// 輸出
呼叫這個 repeatedFunc ("hellworld"),會 alert4 次 helloworld, 每次間隔 3 秒
複製程式碼
某一種解決辦法如下
function repeat(func, times, wait) {
return function () {
let timer = null
const args = arguments
let i = 0;
timer = setInterval(()=>{
while (i >= times) {
clearInterval(timer)
return
}
i++
func.apply(null, args)
}, wait)
}
}
複製程式碼
3.2 某條-請用 JS 實現 throttle(函式節流)函式
函式節流解釋:對函式執行增加一個控制層,保證一段時間內(可配置)內只執行一次。此函式的作用是對函式執行進行頻率控制,常用於使用者頻繁觸發但可以以更低頻率響應的場景
如上圖,在一段時間內函式觸發了 9 次,實際只執行了 5 次,且每次執行的時間間隔不小於 100ms;其中一種解決辦法:
function debounce (fn, time) {
let first = true
let timer = null
return function (...args) {
if (first) {
first = false
fn.apply(this, args)
}
timer = setTimeout(() => {
fn.apply(this, args)
}, 100)
}
}
複製程式碼
謝謝閱讀, 歡迎大家繼續補充