從零開始再學 JavaScript 定時器

Rick_Lee發表於2019-10-14

JavaScript 定時器

1.導讀

在寫 setTimeoutsetInterval 程式碼時,你是否有想過一下幾點:

  • 他們是怎麼實現的?
  • 面試時如果問你原理怎麼回答?
  • 為什麼要了解定時器原理?

首先 setTimeoutsetInterval 都不是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(函式節流)函式

函式節流解釋:對函式執行增加一個控制層,保證一段時間內(可配置)內只執行一次。此函式的作用是對函式執行進行頻率控制,常用於使用者頻繁觸發但可以以更低頻率響應的場景

從零開始再學 JavaScript 定時器
如上圖,在一段時間內函式觸發了 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)
    }
}

複製程式碼

謝謝閱讀, 歡迎大家繼續補充

參考文獻

相關文章