在寫 setTimeout
和 setInterval
代碼時,你是否有想過一下幾點:javascript
首先 setTimeout
和 setInterval
都不是ECMAScript
規範或者任何JavaScript
實現的一部分。它是由瀏覽器實現,而且在不一樣的瀏覽器也會有所差別。定時器也能夠由 Nodejs
運行時自己實現。前端
在瀏覽器中,定時器是 Window
對象下的 api,因此能夠直接在控制檯進行直接調用。java
在 Nodejs
中,定時器是 global
對象的一部分,這點和瀏覽器的 Window
相似。具體能夠去查看下node-timers源碼node
有些人確定會想,爲何必定要了解這些糟糕無聊的原理,咱們只須要運用別人 api 進行開發不就能夠了。很遺憾的告訴你,做爲一名 JavaScript
開發人員,我認爲若是你只是想一直作一個初級開發工程師,那麼你能夠不去了解,若是想要提高,若是不去了解,那可能代表你並不徹底理解V8(和其餘虛擬機)如何與瀏覽器和Node交互。git
本文會經過案例來說解 JavaScript 定時器,還會講解某條的一些面試題github
// eg1.js
setTimeout(
() => {
console.log('Hello after 4 seconds');
},
4 * 1000
);
複製代碼
上面這個例子用 setTimeout
延時 4 秒打印問候語。 若是你在node環境執行 example1.js。Node將會暫停4秒而後打印問候語(接着退出)。面試
setTimeout
第一個參數function - 是你想要在到期時間(delay毫秒)以後執行的函數。【注意:】 setTimeout
的第一個參數只是一個函數引用。 它沒必要像eg1.js
那樣是內聯函數。 這是不使用內聯函數的相同示例:api
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」bash
使用您到目前爲止學到的關於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 秒後的第二條消息。
若是要求你每隔 4秒 打印一條消息怎麼辦? 雖然你能夠將setTimeout
放在一個循環中,但定時器API
也提供了setInterval
函數,這將完成永遠作某事的要求。
// example3.js
setInterval(
() => console.log('Hello every 4 seconds'),
4000
);
複製代碼
此示例將每4秒打印一次消息。 使用 node 命令執行 example3.js 將使 Node 永遠打印此消息,直到你終止該進程.
對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 原理 裏面講的很深。
編寫腳本每秒打印消息「 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。
當你在常規函數中使用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關鍵字時纔有意義。 若是您使用箭頭函數,則根本不須要擔憂調用者。
以1秒的延遲開始,而後每次將延遲增長1秒。 第二次將延遲2秒。 第三次將延遲3秒,依此類推。
在打印的消息中包含延遲時間。 預期輸出看起來像:
Hello World. 1
Hello World. 2
Hello World. 3...
複製代碼
【注意】你只能使用const來定義變量。 你不能使用 let 或 var。 咱們先進行分析以下:
setInterval
,但咱們能夠在遞歸調用中使用setTimeout
手動建立一個間隔執行。 使用setTimeout
的第一個執行函數將建立另外一個計時器,依此類推。如下是解決問題的一種方法:
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);
複製代碼
使用 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)
}
}
複製代碼
函數節流解釋:對函數執行增長一個控制層,保證一段時間內(可配置)內只執行一次。此函數的做用是對函數執行進行頻率控制,經常使用於用戶頻繁觸發但能夠以更低頻率響應的場景
如上圖,在一段時間內函數觸發了 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)
}
}
複製代碼
謝謝閱讀, 歡迎你們繼續補充