先從一個面試題開始:
JavaScript 面試題:setTimeout 和 setInterval 的原始碼是在哪裡實現的? (不能百度和谷歌 )
在繼續往下看之前,請先在你的頭腦中回答問題
你的答案可能會是 V8(或其他VM),但很遺憾,這是錯的。儘管 「JavaScript Timers」 的應用很廣泛,但是 setTimeout
和 setInterval
之類的函數並不是 ECMAScript 規範或任何一種 JavaScript 引擎實現的一部分。Timer 函數是由瀏覽器實現的,不同瀏覽器的實現方式會有所不同。同時 Timer 也是由 Node.js 執行時本身實現的。
在瀏覽器中,主要的計時器函數是 Window
介面的一部分,這個介面還有一些其他函數和物件。該介面使其所有元素在主 JavaScript 作用域內全域性可用。這就是為什麼你可以直接在瀏覽器的控制檯中執行 setTimeout
的原因。
在 Node 中,計時器是 global
物件的一部分,該物件的行為類似於瀏覽器的 window
。你可以在 Node 的原始碼中找到它的實現。
有些人可能認為這個面試題不咋樣,但是我認為你應該瞭解這個,因為如果你不瞭解這一點,則可能表明你並不完全瞭解 V8(及其他VM)是如何與瀏覽器和 Node 互動的。
以下是一些關於計時器函數的例子和編碼挑戰的練習:
定時器函數是高階函數,可用於延遲或重複其他函數的執行(它們作為第一個引數)。
以下是延遲執行的例子:
// example1.js setTimeout( () => { console.log('Hello after 4 seconds'); }, 4 * 1000 );
本例用 setTimeout
將 console.log
的輸出延遲4秒。 setTimeout
的第二個引數是延遲時間(以毫秒為單位)。這就是為什麼要用 4 乘以 1000 的原因。
setTimeout
的第一個引數是你想要延遲執行的函數。
如果用 node
命令執行 example1.js
檔案,Node 會暫停 4。秒鐘,然後輸出一行訊息,之後退出。
注意,setTimeout
的第一個引數只是一個函數參照。也可以不像 example1.js
那樣使用行內函式。下面是不用行內函式的相同功能的程式碼:
const func = () => { console.log('Hello after 4 seconds'); }; setTimeout(func, 4 * 1000);
如果要讓用 setTimeout
延遲執行的函數接受引數,可以將 setTimeout
本身其餘的引數用於將引數值傳遞給所延遲的函數。
// 函數: func(arg1, arg2, arg3, ...) // 可以用: 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 條內容。
要求:
你只能定義一個函數,這其中包括行內函式。這意味著你所有的 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,可以把計時器 ID 當做引數傳給 clearTimeout
函數來取消它。下面一個例子:
// example4.js const timerId = setTimeout( () => console.log('你看不到這行輸出!'), 0 ); clearTimeout(timerId);
這個簡單的計時器應該在 0
毫秒後被觸發(使其立即生效),但實際上並不會,因為此時我們正在獲取 timerId
值,並在呼叫 clearTimeout
之後立即將其取消。
用 node
命令執行 example4.js
時,Node 不會輸出任何內容,而程式將會退出。
順便說一句,在 Node.js 中,還有另一種方法對 0
ms 進行 setTimeout
。 Node.js 計時器 API 還有一個名為 setImmediate
的函數,它與前面 0
毫秒的 setTimeout
基本上相同,但是不用指定延遲時間:
setImmediate( () => console.log('我等效於 0 毫秒的 setTimeout'), );
setImmediate
函數並非在所有瀏覽器中都可用。千萬不要用在前端程式碼中。
和 clearTimeout
類似,還有一個 clearInterval
函數,除了對 setInerval
的呼叫外,它們的功能相同,而且也有 clearImmediate
的呼叫。
在上一個例子中,你可能注意到了,如果用 setTimeout
在 0
毫秒之後執行某個操作,並不意味著會馬上執行它(在 setTimeout
這一行之後),而是在指令碼中的所有其他內容( clearTimeout
這一行)之後才會執行它的呼叫。
// example5.js setTimeout( () => console.log('Hello after 0.5 seconds. MAYBE!'), 500, ); for (let i = 0; i < 1e10; i++) { // 同步阻塞 }
在這個例子中定義了計時器之後,我們立即通過一個大的 for
迴圈來阻塞執行。 1e10
的意思是 1 前面有 10 個零,所以這個迴圈是 100 億次迴圈(基本上模擬了繁忙的 CPU)。在迴圈時 Node 無法執行任何操作。
當然,這在實際開發中非常糟糕,但是它能幫你瞭解 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。
延遲函數將輸出訊息並使計數器加 1。在函數內部的 if
語句中檢查現在是否已經輸出 5 次了,如果是的話,則輸出「Done」並用 intervalId
常數清理。間隔延遲為 1000
毫秒。
當在常規函數中使用 JavaScript 的 this
關鍵字時,如下所示:
function whoCalledMe() { console.log('Caller is', this); }
在關鍵字 this
中的值將代表函數的呼叫者。如果你在 Node REPL 內定義以上函數,則呼叫方將是 global
物件。如果在瀏覽器的控制檯中定義函數,則呼叫方將是 window
物件。
下面把函數定義為物件的屬性,這樣可以看的更加清楚:
const obj = { id: '42', whoCalledMe() { console.log('Caller is', this); } }; // 現在,函數參照為:obj.whoCallMe
現在,當你直接用其參照去呼叫 obj.whoCallMe
函數時,呼叫者將是 obj
物件(由其 ID 進行標識):
現在的問題是,如果把 obj.whoCallMe
的參照傳遞給 setTimetout
呼叫,呼叫者將會是誰?
// 將會輸出什麼? 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);
編寫一段指令碼,用和挑戰#3 相同的可變延遲概念連續輸出訊息 「Hello World」,但是這次,每個主延遲間隔以 5 條訊息為一組。前 5 條訊息的延遲為 100ms,然後是下 5 條訊息的延遲為 200ms,然後是 300ms,依此類推。
指令碼的行為如下:
在輸出的訊息中包含延遲。預期的輸出如下所示(不帶註釋):
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);
相關免費學習推薦:
以上就是你需要知道的關於javascript計時器的所有內容的詳細內容,更多請關注TW511.COM其它相關文章!