setTimeout實現原理和使用注意

setTimeout,它就是一個定時器,用來指定某個函數在多少毫秒以後執行。前端

setTimeout用法web

var timeoutID = setTimeout(function[, delay, arg1, arg2, ...]);
var timeoutID = setTimeout(function[, delay]);
var timeoutID = setTimeout(code[, delay]);
  • 第一個參數爲函數或可執行的字符串(好比alert('test'),此法不建議使用)
  • 第二個參數爲延遲毫秒數,可選的,默認值爲0.
  • 第三個及後面的參數爲函數的入參。
  • setTimeout 的返回值是一個數字,這個值爲timeoutID,能夠用於取消該定時器。

    setTimeout在瀏覽器中的實現

瀏覽器渲染進程中全部運行在主線程上的任務都須要先添加到消息隊列,而後事件循環系統再按照順序執行消息隊列中的任務。瀏覽器

在 Chrome 中除了正常使用的消息隊列以外,還有另一個消息隊列(咱們能夠稱爲延遲隊列),這個隊列中維護了須要延遲執行的任務列表,包括了定時器和 Chromium 內部一些須要延遲執行的任務。因此當經過 JavaScript 建立一個定時器時,渲染進程會將該定時器的回調任務添加到延遲隊列中。微信

好比這樣的一段代碼:markdown

function foo(){
  console.log("test")
}
var timeoutID = setTimeout(foo,100);

當經過 JavaScript 調用 setTimeout 設置回調函數的時候,渲染進程將會建立一個回調任務,包含了回調函數foo、當前發起時間、延遲執行時間等,其模擬代碼以下所示:dom

struct DelayTask{
  int64 id;
  CallBackFunction cbf;
  int start_time;
  int delay_time;
};
DelayTask timerTask;
timerTask.cbf = foo;
timerTask.start_time = getCurrentTime(); //獲取當前時間
timerTask.delay_time = 100;//設置延遲執行時間

建立好回調任務以後,就會將該任務添加到延遲執行隊列中。那這個回調任務,何時會被執行呢?
瀏覽器中有個函數是專門用來處理延遲執行任務的,暫且稱爲ProcessDelayTask,它的主要邏輯以下:ide

void ProcessTimerTask(){
  //從delayed_incoming_queue中取出已經到期的定時器任務
  //依次執行這些任務
}

TaskQueue task_queue;
void ProcessTask();
bool keep_running = true;
void MainTherad(){
  for(;;){
    //執行消息隊列中的任務
    Task task = task_queue.takeTask();
    ProcessTask(task);

    //執行延遲隊列中的任務
    ProcessDelayTask()

    if(!keep_running) //若是設置了退出標誌,那麼直接退出線程循環
        break; 
  }
}

其實就是,當瀏覽器處理完消息隊列中的一個任務以後,就會開始執行 ProcessDelayTask 函數。ProcessDelayTask 函數會根據發起時間和延遲時間計算出到期的任務,而後依次執行這些到期的任務。等到期的任務執行完成以後,再繼續下一個循環過程。這樣定時器就實現了,從這個過程也能夠明顯看出,定時器並不必定是準時延後執行的。函數

注意事項

  1. 若是當前任務執行時間太久,會延遲到期定時器任務的執行
    在使用 setTimeout 的時候,有不少因素會致使回調函數執行比設定的預期值要久,其中一個就是上文說到的,若是處理的當前任務耗時過長,定時器設置的任務就會被延後執行。
    好比在瀏覽器中執行這樣一段代碼,並打印執行時間:
function bar() {
    console.log('bar')
    const endTime = Date.now()
    console.log('cost time',endTime - startTime)
}
function foo() {
    setTimeout(bar, 0);
    for (let i = 0; i < 5000; i++) {
        let i = 5+8+8+8
        console.log(i)
    }
}
foo()

執行結果如圖:oop

setTimeout實現原理和使用注意
從結果能夠看到,執行 foo 函數所消耗的時長是 365 毫秒,這也就意味着經過 setTimeout 設置的任務被推遲了 365 毫秒才執行,而設置 setTimeout 的回調延遲時間是 0。學習

  1. 使用 setTimeout 設置的回調函數中的 this 環境不是指向回調函數
好比這段代碼:
var name= 1;
var MyObj = {
  name: 2,
  test:1,
  showName: function(){
    console.log(this.name,this.test);
  }
}
setTimeout(MyObj.showName,1000)
MyObj.showName()
//先輸出 2 1
// 1s後輸出 1 undefined

這裏其實認真分析一下,也很好理解這個 this 的指向。按照 this 的規定,若是是對象調用(obj.fn()),那麼this指向該對象,所以MyObj.showName()輸出的是 MyObj 裏面的值。在 setTimeout 中,入參是MyObj.showName,這裏是把這個值傳了進去,能夠理解爲:

const fn = MyObj.showName
setTimeout(fn,1000)

這樣看,在setTimeout裏面,當執行到的時候,實際上就是在window下執行fn,此時的this,就指向了window,而不是原來的函數。

  1. setTimeout 存在嵌套調用問題
    若是 setTimeout 存在嵌套調用,調用超過5次後,系統會設置最短執行時間間隔爲 4 毫秒。
    咱們能夠在瀏覽器粗略測試一下,有以下代碼:
let startTime = Date.now()
function cb() { 
  const endTime = Date.now()
  console.log('cost time',endTime - startTime)
  startTime = startTime
  setTimeout(cb, 0); 
}
setTimeout(cb, 0);

執行結果:

setTimeout實現原理和使用注意
從結果能夠看出,前面五次調用的時間間隔比較小,嵌套調用超過五次以上,後面每次的調用最小時間間隔是 4 毫秒(我運行的結果,間隔基本是 5ms,考慮有代碼執行的計算偏差)。

之因此出現這樣的狀況,是由於在 Chrome 中,定時器被嵌套調用 5 次以上,系統會判斷該函數方法被阻塞了,若是定時器的調用時間間隔小於 4 毫秒,那麼瀏覽器會將每次調用的時間間隔設置爲 4 毫秒。能夠看下源碼(https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/frame/dom_timer.cc)

static const int kMaxTimerNestingLevel = 5;

// Chromium uses a minimum timer interval of 4ms. We'd like to go
// lower; however, there are poorly coded websites out there which do
// create CPU-spinning loops.  Using 4ms prevents the CPU from
// spinning too busily and provides a balance between CPU spinning and
// the smallest possible interval timer.
static constexpr base::TimeDelta kMinimumInterval = base::TimeDelta::FromMilliseconds(4);

因此,一些實時性較高的需求就不太適合使用 setTimeout 了,好比你用 setTimeout 來實現 JavaScript 動畫就不必定是一個很好的主意。

  1. 未激活的頁面,setTimeout 執行最小間隔是 1000 毫秒
    若是標籤不是當前的激活標籤,那麼定時器最小的時間間隔是 1000 毫秒,目的是爲了優化後臺頁面的加載損耗以及下降耗電量。這一點你在使用定時器的時候要注意。

  2. 延時執行時間有最大值
    Chrome、Safari、Firefox 都是以 32 個 bit 來存儲延時值的,32bit 最大隻能存放的數字是 2147483647 毫秒,這就意味着,若是 setTimeout 設置的延遲值大於 2147483647 毫秒(大約 24.8 天)時就會溢出,這致使定時器會被當即執行。如:
let startTime = Date.now()
function foo(){
  const endTime = Date.now()
  console.log('cost time',endTime - startTime)
  console.log("test")
}
var timerID = setTimeout(foo,2147483648);//會被當即調用執行

執行結果:

setTimeout實現原理和使用注意
運行後能夠看到,這段代碼是當即被執行的。但若是將延時值修改成小於 2147483647 毫秒的某個值,那麼執行時就沒有問題了。

參考資料

  • 極客時間《瀏覽器工做原理與實踐》

最後

  • 歡迎加我微信(winty230),拉你進技術羣,長期交流學習...
  • 歡迎關注「前端Q」,認真學前端,作個有專業的技術人...
    setTimeout實現原理和使用注意
相關文章
相關標籤/搜索