Scheduling: setTimeout and setInterval

setTimeout

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

// 1 秒後執行函式
function sayHi() {
  alert('Hello');
}
setTimeout(sayHi, 1000);

// 帶入參數到函式
function sayHi(phrase, who) {
  alert( phrase + ', ' + who );
}
setTimeout(sayHi, 1000, "Hello", "John"); // Hello, John

// 如果第 1 個參數是字串會視為函式,但不推薦
setTimeout("alert('Hello')", 1000);

// 這樣的寫法比較好
setTimeout(() => alert('Hello'), 1000);

// setTimeout 要帶入函式而非結果
// wrong!
setTimeout(sayHi(), 1000);

Canceling with clearTimeout

// 取消 setTimeout 的設定
let timerId = setTimeout(() => alert("never happens"), 1000);
alert(timerId); // timer identifier

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

setInterval

// 每隔一段一時間就會執行一次
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)

// repeat with the interval of 2 seconds
let timerId = setInterval(() => alert('tick'), 2000);

// after 5 seconds stop
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);

// 當出現 alert/confirm/prompt,秒數仍在計算

Recursive setTimeout

// 規律性執行程式碼有 2 種選擇,一種用 setInterval,另一種用 recursive setTimeout。
/** instead of:
let timerId = setInterval(() => alert('tick'), 2000);
*/

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

// 使用 recursive setTimeout 比較靈活,可以根據情況設定下一次執行時間。
let delay = 5000;

let timerId = setTimeout(function request() {
  ...send request...

  if (request failed due to server overload) {
    // increase the interval to the next run
    delay *= 2;
  }

  timerId = setTimeout(request, delay);

}, delay);

// 透過 recursive setTimeout 可以精準執行,setInterval 無法

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

透過 recursive setTimeout 每次延遲時間可以精準執行,setInterval 無法。

// setInterval
let i = 1;
setInterval(function() {
  func(i);
}, 100);

// 如果函式執行時間超過延遲時間,JavaScript 引擎會等待函式執行完然後立刻執行下一個函式。
// 因為執行函式的時間被計算在延遲時間內,造成延遲時間不能精確執行。

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

// setTimeout 會等函式執行完才開始計算延遲時間,所以可以準確執行。

Garbage collection

// 當 setTimeout/setInterval 沒有被清除,會一直存在記憶體內不會消失,如果函式指向外部變數
// 外部變數也不會消失,這會造成效能問題
// the function stays in memory until the scheduler calls it
setTimeout(function() {...}, 100);

setTimeout(…,0)

// setTimeout(func) 等目前程式碼執行完畢才會執行,換言之是異步函式
setTimeout(() => alert("World"));
alert("Hello");
// Hello
// World

Splitting CPU-hungry tasks

// 如果有一個很吃資源的執行腳本,會造成整個網頁當掉,這時可以分開程式碼,像是前 100 行先執行
// 後 100 行用 setTimeout 執行
// 下面程式碼執行時網頁當掉
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();

// 改成這樣執行時網頁仍可運作
// 第一次運算 i=1...1,000,000,第二次運算 i=1,000,001..2,000,000,直到 i=1,000,000,000
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); // schedule the new call (**)
  }
}
count();

// 在開始執行前就先預設新的呼叫函式,這樣耗費時間較少。
let i = 0;

let start = Date.now();

function count() {

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

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

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

}

count();

Minimal delay of nested timers in-browser

// 在網頁端 HTML5 規定,經過 5 次巢狀 setTimeout 函式延遲時間至少為 4 ms,
// 但在 server 端沒有這個限制
let start = Date.now();
let times = [];

setTimeout(function run() {
  times.push(Date.now() - start); // 保存上次调用的延时

  if (start + 100 < Date.now()) alert(times); // 100 毫秒之后,显示延时信息
  else setTimeout(run, 0); // 没超过 100 毫秒则再进行调度
}, 0);

// 2,3,5,8,13*,19,24,29,35,40,44,49,55,60,65,71,76,81,87,97,101

Allowing the browser to render

// 執行一個很大的腳本,當更改網頁內容時,會等到函式執行完才顯示解果,用 setTimeout 可以觀察到
// 內容持續改變,可以用來展示進度。
// normal
<div id="progress"></div>

<script>
  let i = 0;

  function count() {
    for (let j = 0; j < 1e6; j++) {
      i++;
      // 将当前 i 值放到 <div> 内
      // (innerHTML 在以后具体章节会讲到,这行代码看懂应该没问题)
      progress.innerHTML = i;
    }
  }

  count();
</script>

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

<script>
  let i = 0;

  function count() {

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

    if (i < 1e9) {
      setTimeout(count);
    }

  }

  count();
</script>

Last updated