導語:JavaScript定時器是window的一個對象介面,並不是JavaScript的一部分,它的功能是由瀏覽器實現的,在不同瀏覽器之間會有所不同。定時器也可以由node.js運行時本身實現。 幾周前我在推特上發佈了這樣一個面試問題: JavaScript面試問題: 在哪裡可以找到setTime ...
導語:JavaScript定時器是window的一個對象介面,並不是JavaScript的一部分,它的功能是由瀏覽器實現的,在不同瀏覽器之間會有所不同。定時器也可以由node.js運行時本身實現。
幾周前我在推特上發佈了這樣一個面試問題:
JavaScript面試問題:
在哪裡可以找到setTimeout和setInterval的源代碼?(他們在哪裡實現的?)
你怎麼在面試中回答?(你不能去網上搜索)
function setTimeOut(callback

繼續往下看之前先試著回答這個問題
推特上半數的回答都是錯誤的 回答不是 V8 (或者其他虛擬機!!)儘管著名的“JavaScript定時器”函數像setTimeout
和 setInterval
都不是ECMAScript規範或者任何JavaScript實現的一部分。 定時器功能由瀏覽器實現,它們的實現在不同瀏覽器之間會有所不同。定時器也可以由Node.js運行時本身實現。
在瀏覽器里主要的定時器函數是作為Window
對象的介面,Window
對象同時擁有很多其他方法和對象。該介面使其所有元素在JavaScript全局作用域中都可用。這就是為什麼你可以直接在瀏覽器控制台執行setTimeout
。
在node里,定時器是global
對象的一部分,這點很像瀏覽器中的Window
。你可以在Node里看到定時器的源碼 這裡.
有些人可能認為這是一個糟糕的面試問題 - 為什麼一定要知道這個問題呢?!作為一名JavaScript開發人員,我認為你應該知道這一點,因為如果你不這樣做,那可能表明你並不完全理解V8(和其他虛擬機)如何與瀏覽器和Node交互。
讓我們開始做一些關於定時器函數的例子和挑戰把?
更新: 這篇文章現在是“完整介紹Node.js”的一部分。 你可以在這裡閱讀最新的版本。
定時器函數是高階函數,可用於延遲或重覆執行其他函數(它們作為第一個參數接收)。
這是一個關於延遲的例子:
// example1.js setTimeout( () => { console.log('Hello after 4 seconds'); }, 4 * 1000 );
這個例子用setTimeout
延時4秒列印問候語。setTimeout
的第二個參數是延時(多少毫秒)。這就是為什麼我用4*1000來表示4秒
setTimeout
的第一個參數是一個將被延遲執行的函數
如果你在node
環境執行 example1.js
。Node將會暫停4秒然後列印問候語(接著退出)。
請註意,setTimeout
的第一個參數只是一個函數引用。 它不必像example1.js
那樣是內聯函數。 這是不使用內聯函數的相同示例:
const func = () => { console.log('Hello after 4 seconds'); }; setTimeout(func, 4 * 1000);
如果使用setTimeout
延遲的函數需要攜帶參數,我們可以把參數放在setTimeout
里(放在已知的兩個參數後)來中轉參數給需要延遲執行的函數。
// 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”
使用您到目前為止學到的關於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
函數,這將完成永遠做某事的要求。
這是setInterval的一個例子:
// example3.js setInterval( () => console.log('Hello every 3 seconds'), 3000 );
此示例將每3秒列印一次消息。 使用node
命令執行example3.js
將使Node永遠列印此消息,直到你終止該進程(使用CTRL + C)。
因為調用計時器函數會調度操作,所以在執行之前也可以取消該操作。
對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
延遲不是一個保證的東西,而是一個最小的東西。 500
ms表示最小延遲為500
ms。 實際上,腳本將花費更長的時間來列印其問候語。 它必須等待阻塞迴圈才能完成。
編寫腳本每秒列印消息“ 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
關鍵字時才有意義。 如果您使用箭頭函數,則根本不需要擔心調用者。
編寫腳本以連續列印具有不同延遲的消息“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語句。
解決方案
因為我們只能使用setInterval
調用,所以我們還需要遞歸來增加下一個setInterval
調用的延遲。 另外,我們需要一個if語句來控制只有在5次調用該遞歸函數之後才能執行此操作。
以下是其中一種解決方案:
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);
謝謝閱讀。