導語: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延遲不是一個保證的東西,而是一個最小的東西, 500ms表示最小延遲為500ms, 實際上,腳本將花費更長的時間來列印其問候語, 它必須等待阻塞回圈才能完成,
撰寫腳本每秒列印訊息“ 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);
謝謝閱讀,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/93704.html
標籤:JavaScript
