javascript 定時器工作原理

jopen 9年前發布 | 11K 次閱讀 JavaScript開發 JavaScript

說到 javascript 中的定時器,我們肯定會想到setTimeout()和setInterval()這兩個函數。本文將從 事件循環(Event Loop) 的角度來分析兩者的工作原理和區別。

setTimeout()

MDN對setTimeout的定義為:

在指定的延遲時間之后調用一個函數或執行一個代碼片段。

語法

setTimeout的語法非常簡單,第一個參數為回調函數,第二個參數為延時的時間。函數返回一個數值類型的ID唯一標示符,此ID可以用作clearTimeout的參數來取消定時器:

var timeoutID = window.setTimeout(code, delay);

IE0+ 還支持回調參數的傳入:

var timeoutID = window.setTimeout(func, delay, [param1, param2, ...]);

setInterval()

MDN 對 setInterval 的定義為:

周期性地調用一個函數(function)或者執行一段代碼。

由于setInterval和setTimeout的用法一樣,所以這里不再列出。

對第二個參數(delay)的說明

由于javascript 的事件循環機制,導致第二個參數并不代表延遲delay毫秒之后立即執行回調函數,而是嘗試將回調函數加入到事件隊列。實際上,setTimeout和setInterval在這一點上處理又存在區別:

  • setTimeout:延時delay毫秒之后,啥也不管,直接將回調函數加入事件隊列。

  • setInterval: 延時delay毫秒之后,先看看事件隊列中是否存在還沒有執行的回調函數(setInterval的回調函數),如果存在,就不要再往事件隊列里加入回調函數了。

所以,當我們的代碼中存在耗時的任務時,定時器并不會表現的如我們所想的那樣。

通過一個例子來理解

下面的代碼,本來希望能夠在100ms和200ms的時候(也就是剛好等待100ms)調用回調函數:

var timerStart1 = now();
setTimeout(function () {
  console.log('第一個setTimeout回調執行等待時間:', now() - timerStart1);

  var timerStart2 = now();
  setTimeout(function () {
    console.log('第二個setTimeout回調執行等待時間:', now() - timerStart2);
  }, 100);
}, 100);
// 輸出:
// 第一個setTimeout回調執行等待時間: 106
// 第二個setTimeout回調執行等待時間: 107

這樣的結果看上去正是我們所想的那樣,但是一旦我們在代碼中加入了耗時的任務時候,結果就不像我們所期望的那樣了:

var timerStart1 = now();
setTimeout(function () {
  console.log('第一個setTimeout回調執行等待時間:', now() - timerStart1);

  var timerStart2 = now();
  setTimeout(function () {
    console.log('第二個setTimeout回調執行等待時間:', now() - timerStart2);
  }, 100);

  heavyTask();  // 耗時任務
}, 100);

var loopStart = now();
heavyTask(); // 耗時任務
console.log('heavyTask耗費時間:', now() - loopStart);

function heavyTask() {
  var s = now();
  while(now() - s < 1000) {
  }
}

function now () {
  return new Date();
}
// 輸出:
// heavyTask耗費時間: 1015
// 第一個setTimeout回調執行等待時間: 1018
// 第二個setTimeout回調執行等待時間: 1000

兩個setTimeout的等待事件由于耗時任務的存在不再是100ms了!我們來描述一下事情的經過:

  1. 首先,第一個耗時任務(heavyTask())開始執行,它需要大約1000ms才能執行完畢。

  2. 從耗時任務開始執行,過了100ms, 第一個setTimeout的回調函數期望執行,于是被加入到事件隊列,但是此時前面的耗時任務還沒執行完,所以它只能在隊列中等待,直到耗時任務執行完畢它才開始執行,所以結果中我們開的看到的是:第一個setTimeout回調執行等待時間: 1018。

  3. 第一個setTimeout回調一執行,又開啟了第二個setTimeout, 這個定時器也是期望延時100ms之后能夠執行它的回調函數。 但是,在第一個setTimeout又存在一個耗時任務,所有它的劇情跟第一個定時器一樣,也等待了 1000ms 才開始執行。

可以用下面的圖來概括:
setTimeout

再來看setInterval的一個例子:

var intervalStart = now();
setInterval(function () {
  console.log('interval距定義定時器的時間:', now() - loopStart);
}, 100);

var loopStart = now();
heavyTask();
console.log('heavyTask耗費時間:', now() - loopStart);

function heavyTask() {
  var s = now();
  while(now() - s < 1000) {
  }
}

function now () {
  return new Date();
}
// 輸出:
// heavyTask耗費時間: 1013
// interval距定義定時器的時間: 1016
// interval距定義定時器的時間: 1123
// interval距定義定時器的時間: 1224

上面這段代碼,我們期望每隔100ms就打出一條日志。相對于setTimeout的區別,setInterval在準備把回調函數加入到事件隊列的時候,會判斷隊列中是否還有未執行的回調,如果有的話,它就不會再往隊列中添加回調函數。 不然,會出現多個回調同時執行的情況。

可以用下面的圖來概括:
setInterval

總結

上面對javascript定時器執行原理進行了簡要的分析,希望能夠幫助我們更深入的理解javascript。文中有描述不當的地方可以在評論中指出。

文章地址:http://blog.mcbird.cn/2015/09/18/javascript-timers/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!