天天看點

ARTS Tip1 JavaScript Scheduling setTimeout and setInterval

在開發中,遇到了一種需求,有個接種情況,一支疫苗是多人份的,如果打開了,那麼必須在有效期内注射完,否則這個疫苗就應該廢棄,是以當這支疫苗打開的時候,那麼就需要從有效期的最大時間開始定時,一直到有效期的時間變為0,此時,如果還沒有打完,那麼這個疫苗就要廢棄。

當然我們需要在畫面上顯示這個疫苗什麼時間被打開,以及什麼時候到期,這樣醫生就不用自己看時間,直接從畫面就可以看到這個疫苗什麼時間廢棄,這樣對于客戶來說減輕負擔和工作量,是以我們需要對畫面上一覽查詢的多人份疫苗需要定時的重新整理,比如10s一重新整理,直到他的有效期變為0,同時可能還會有其他種類的多人份疫苗,是以我們需要定時重新整理資料。

第一種想法:在後端使用定時任務,固定時間間隔來加載資料并顯示。

第二種想法:在前端進行定時輪詢,這樣隻有在該畫面打開的時候在會從背景加載資料,不會在項目剛啟動就從背景加載資料。

是以,我選擇了第二種做法。選擇了setInterval定時函數來進行操作。下面我會具體分享一下Scheduling中的setTimeout和setInterval函數

使用場景:在我們不需要立即執行這個函數,需要等待一段時間在執行的時候,我們把這種函數就叫做 定時函數。

分類:

  1. setTimeout

    : 這個函數允許在等待一段時間後再執行(隻執行一次);
  2. setInterval

    : 這個函數允許在固定的時間間隔後規律的反複執行。

這兩個函數并不是JavaScript規範中的一部分,但是大多數環境都有定時器和提供的方法,尤其是,他們支援所有的浏覽器并且支援NodeJs.

setTimeout

函數

文法:

let timerId = setTimeout(func|code, delay[, arg1, arg2...])

參數解釋:

fun|code

:要定時執行的字元串或者函數。通常的話是一個函數,對于一些曆史原因,字元串也可以被傳遞,但是一般不會推薦使用字元串。

delay

: 延時,表示要延時多久以後該函數才被執行,參數值應該是毫秒(1s=1000ms)

arg1,arg2

:函數的參數(這個是不支援IE9及以下版本)

看下面一個例子,這個例子是說,在1s後,會調用

sayHi

函數,并且執行

alert

,代碼示例如下:

function sayHi() {
  alert('Hello');
}

setTimeout(sayHi, 1000);
           

帶參數的示例如下:

function sayHi(phrase, who) {
  alert( phrase + ', ' + who );
}

setTimeout(sayHi, 1000, "Hello", "John");
           

如果第一個參數是字元串,JavaScript會通過它建立一個函數,是以下面的也會被執行:

setTimeout("alert('Hello')", 1000);
           

但是我們并不建議采用字元串的形式,是以下面這樣,使用函數來替代字元串:

setTimeout(() => alert('Hello'), 1000);
           

注意下面的特殊例子,隻可以傳函數名字,不要傳函數名字加(),否則是不可以運作的。

// wrong!
setTimeout(sayHi(), 1000);
           

上述代碼異常,因為

setTimeout

函數希望得到的是一個函數引用,

sayHi()

函數運作後,傳回它的運作結果傳遞給

setTimeout

。在我們的例子中

sayHi()

的結果是

undefined

的。是以不會有任何的東西被定時執行。

clearTimeout

調用

setTimeout

會傳回一個

time identifier

timerId

,我們可以使用

timerId

來取消執行

取消執行的文法如下:

let timerId = setTimeout(...);
clearTimeout(timerId);
           

下面這個例子中,我們定時調用函數,但是我們改變了想法不想再調用,結果就是什麼都不會發生:

let timerId = setTimeout(() => alert("never happens"), 1000);
alert(timerId); // timer identifier

clearTimeout(timerId);
alert(timerId); // same identifier (doesn't become null after canceling)
           

你可以複制上面這段代碼去運作,會發現,輸出的是一個數值,我實驗輸出的是11,浏覽器是chrome。NodeJs傳回一個事件對象。

setInterval

文法:

let timerId = setInterval(func|code, delay[, arg1, arg2...])

setInterval

函數的參數含義和

setTimeout

的含義相同,不同的是,

setInterval

是按照固定的時間間隔定時執行裡面的函數,不是隻執行一次。

如果想停止,我們可以調勇這個方法

clearInterval(timerId)

下面是該方法的示例代碼:

// 間隔兩秒執行一次
let timerId = setInterval(() => alert('tick'), 2000);

// 5s後停止執行
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);
           

當展示

alert/confirm/prompt

的時候,在浏覽器IE和Firefox中内部計時器會繼續執行;但是在chrome和Opera和Safari的的内部計時器就會停止。

是以如果你在以上執行代碼,并且沒有關閉alert視窗。接着在Firefox或者IE中,alert視窗會接着顯示(2s過後)但是在 Chrome/Opera/Safari中可能會等待更多的時間。

遞歸

setTimeout

有兩種形式可以規律性的執行一些代碼。

一個是使用

setInterval

。另外一種形式就是遞歸

setTimeout

let timerId = setTimeout(function tick() {
  alert('tick');
  timerId = setTimeout(tick, 2000); // (*)
}, 2000);
           

在(*)這個結束的時候會開始調用下一個。

遞歸

setTimeout

比使用

setInterval

更加靈活,因為使用前者下次的定時時間可以與上次的不相同,取決于目前調用的結果。

例如,我們需要寫一個服務,這個服務每隔5s發送一個請求給伺服器請求資料,但是為了防止服務過載,他應該增加時間間隔,比如10s,20s,30s,40s…

let delay = 5000;

let timerId = setTimeout(function request() {
  if (request failed due to server overload) {
    // 下次執行的時候增加時間間隔
    delay *= 2;
  }
  timerId = setTimeout(request, delay);
}, delay);
           

如果我們經常有CPU需求,那麼我們可以測量執行所花費的時間,并計劃下一次調用。

遞歸調用

setTimeout

可以保證執行期間有延時,但是

setInterval

是不可以的。

來看下面兩個比較:

setInterval

let i = 1;
setInterval(function() {
  func(i);
}, 100);
           
ARTS Tip1 JavaScript Scheduling setTimeout and setInterval

遞歸

setTimeout

let i = 1;
setTimeout(function run() {
  func(i);
  setTimeout(run, 100);
}, 100);
           

對于

setInterval

,内部定時器會每隔100ms執行一次

func(i)

.

真正的延時在

setInterval

中調用

func

的時間是少于在代碼中給定的時間的。那是正常的,因為

func

的執行也會消耗一部分時間。很可能

func

的執行結果證明比我們所期待的時間更多,時間會大于100ms。在這種情況下,引擎會等待

func

完成,然後檢查排程程式以及時間是否結束,如果結束就再次立即執行。另外一種情況就是,函數總是執行比

delay

設定的時間更長,結果就是中間都沒有停止就會繼續執行下一次調用。下面是一個遞歸調用

setTimeout

的圖檔解釋:

ARTS Tip1 JavaScript Scheduling setTimeout and setInterval

遞歸

setTimeout

可以保證固定的時間間隔。(這裡是100ms)

那是因為一個新的調用計劃在前一個的末尾等待。

垃圾收集

當一個函數被傳遞給

setInterval

或者

setTimeout

的時候,一個内部的引用就會被建立并且儲存在定時器中。它可以保護該函數防止被當做垃圾進行收集,即使這裡沒有其他引用到它。

對于

setInterval

函數來說,它會一直待在記憶體中直到

clearInterval

方法被調用。

這裡也存在一個負面影響,一個函數的引用的外部變量的環境中時,當它存活的時候,外部變量也同樣存活。它們或許比函數本身占用了更多的記憶體。是以當我們不需要定時函數的時候,最好的辦法是停止它,即使定時函數很小。

setTimeout(…,0)

這是一個特殊的使用方法:

setTimeout(func,0)

這個定時是盡可能快的執行,因為定時器在目前的代碼執行完以後就會調用它。換句話說就是異步執行。

例如,下面輸出是

Hello

,然後立即輸出

World

setTimeout(() => alert("World"), 0);

alert("Hello");

           

為什麼是這樣?因為第一行“在0ms後調用會放在月曆中”。 但是排程程式隻會在目前代碼完成後“檢查月曆”,是以“Hello”是第一個,而“World”是在它之後。

拆分CPU饑餓任務(splitting cpu-hungry tasks)

對于CPU饑餓任務,我們有一個技巧,可以使用

setTimeout

來解決。

例如,文法高亮腳本(用于對此頁面上的代碼示例進行着色)非常耗費CPU。對于高亮代碼,它扮演這分析,建立很多彩色元素,并把他們添加到文檔中,這會消耗很多。甚至會導緻浏覽器奔潰。

是以我們可以将這個又長又大的文本碎片化,第一次處理100行,然後計劃處理下100行使用

setTimeout(...,0)

,諸如此類。

為了清楚起見,讓我們對我們的考慮做一個簡單的例子。我們有個函數,作用是從1計數到1000000000.

如果你運作它,CPU将會挂起。對于明顯引人注目的伺服器端JS,如果您在浏覽器中運作它,然後嘗試單擊頁面上的其他按鈕 - 您将看到整個JavaScript實際上已暫停,在完成之前對于任何其他操作都是無效的。

let i = 0;

let start = Date.now();

function count() {

  // do a heavy job
  for (let j = 0; j < 1e9; j++) {
    i++;
  }

  alert("Done in " + (Date.now() - start) + 'ms');
}

count();
           

浏覽器或許會給出提示,說是腳本運作時間太長的警告。(希望不要出來,因為數字并不是很大)。下面使用

setTimeout

來解決:

let i = 0;

let start = Date.now();

function count() {

  // do a piece of the heavy job (*)
  do {
    i++;
  } while (i % 1e6 != 0);

  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  } else {
    setTimeout(count, 0); // schedule the new call (**)
  }

}

count();
           

現在浏覽器在計數期間正常運作。

我們在(*)做了下面這個工作:

首先執行了i從1到1000000,然後又執行1000001到2000000,以此類推,用

while

來判斷i是否可以被1000000相除。

如果我們到現在還沒有做,那麼下次調用會在(**)處進行。

計數執行之間的暫停為JavaScript引擎提供足夠的“呼吸”以執行其他操作,以對其他使用者操作做出反應。

值得注意的是,兩種變體 - 無論是否通過setTimeout分割作業 - 都具有可比性。 總計數時間沒有太大差異。

為了讓它們更接近,讓我們改進一下。

我們将在count()的開頭調用:

let i = 0;

let start = Date.now();

function count() {

  // move the scheduling at the beginning
  if (i < 1e9 - 1e6) {
    setTimeout(count, 0); // schedule the new call
  }

  do {
    i++;
  } while (i % 1e6 != 0);

  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  }

}

count();
           

現在,當我們開始count()并知道我們需要更多地count()時,我們會在完成工作之前立即安排。

如果你運作它,很容易注意到它花費的時間要少得多。

嵌入式計時器在浏覽器中的延遲最小

在浏覽器中,嵌套計時器運作的頻率存在限制。 HTML5标準說:“在五個嵌套定時器之後,間隔被強制為至少4毫秒

讓我們通過下面的例子示範它的含義。 其中的

setTimeout

調用在0ms後重新排程自身。在次數數組中每次都會從前一個調用的真實時間。 真正的延遲是什麼樣的? 讓我們來看看:

let start = Date.now();
let times = [];

setTimeout(function run() {
  times.push(Date.now() - start); // 記住前一次調用的時間延時。

  if (start + 100 < Date.now()) alert(times); // 在100ms後顯示延時
  else setTimeout(run, 0); // else 重新調用
}, 0);

// 這個例子的輸出結果如下
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
           

可以從結果看出,起初是立即執行,但是後來慢慢的形成了時間間隔9,15,20,24.。。。

這個限制就來自于很久之前,大多腳本都依賴于它,是以這也是它存在的曆史原因。

對于服務端的JavaScript來說,這種限制是不存在的,這裡存在其他的方式來立即異步調用任務,比如在NodeJS中可以用

process.nextTick

setImmediate

。是以這個概念隻是針對浏覽器的。

允許浏覽器呈現

浏覽器裡面的腳本的另一個好處就是可以展示一個進度條或者其它的東西給使用者。那是因為在腳本完成之後,浏覽器通常會全部重新繪制畫面 。

是以,如果我們執行單個巨大的功能,那麼即使它發生了變化,更改也不會反映在文檔中,直到完成為止

<div id="progress"></div>

<script>
  let i = 0;

  function count() {
    for (let j = 0; j < 1e6; j++) {
      i++;
      progress.innerHTML = i;
    }
  }

  count();
</script>
           

當你運作這段代碼的時候,對于i值的改變它将在整個count函數執行完之後才會顯示出來。有個壞處就是畫面必須一直等到count執行完以後才可以點選做其他的操作,下面使用

setTimeout

的話,就不存在這個問題,但是畫面上的i值一直會是變化的。

如果我們使用setTimeout将其拆分為多個部分,則會在運作之間應用更改,是以這看起來更好。

總結:

  1. setInterval(func,delay,...args)

    setTimeout(func,delay,...args)

    都可以執行定時,差別在于前者可以規律的進行執行,後者隻可以執行一次。
  2. 如果定時器不用的話,我們應該對其進行取消,使用

    clearInterval

    或者

    clearTimeout

    ,原因上面提到過,引用外部的變量會随着定時器一直存活下去。
  3. 遞歸調用

    setTimeout

    setInterval

    更加靈活。他們可以保證兩次執行間的最小的時間。
  4. setTimeout(...,0)

    的使用。

參考文章:https://javascript.info/settimeout-setinterval#recursive-settimeout

ARTS Tip1 JavaScript Scheduling setTimeout and setInterval

繼續閱讀