從零開始再學 JavaScript 定時器

JavaScript 定時器

1.導讀

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

  • 他們是怎麼實現的?
  • 面試時若是問你原理怎麼回答?
  • 爲何要了解定時器原理?

首先 setTimeoutsetInterval 都不是ECMAScript規範或者任何JavaScript實現的一部分。它是由瀏覽器實現,而且在不一樣的瀏覽器也會有所差別。定時器也能夠由 Nodejs 運行時自己實現。前端

在瀏覽器中,定時器是 Window 對象下的 api,因此能夠直接在控制檯進行直接調用。java

Nodejs 中,定時器是 global 對象的一部分,這點和瀏覽器的 Window 相似。具體能夠去查看下node-timers源碼node

有些人確定會想,爲何必定要了解這些糟糕無聊的原理,咱們只須要運用別人 api 進行開發不就能夠了。很遺憾的告訴你,做爲一名 JavaScript 開發人員,我認爲若是你只是想一直作一個初級開發工程師,那麼你能夠不去了解,若是想要提高,若是不去了解,那可能代表你並不徹底理解V8(和其餘虛擬機)如何與瀏覽器和Node交互。git

本文會經過案例來說解 JavaScript 定時器,還會講解某條的一些面試題github

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那樣是內聯函數。 這是不使用內聯函數的相同示例: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

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(函數節流)函數

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

如上圖,在一段時間內函數觸發了 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)
    }
}

複製代碼

謝謝閱讀, 歡迎你們繼續補充

參考文獻

相關文章
相關標籤/搜索