setTimeout() 與 setInterval() 的源碼分析

最近筆者在開發的過程當中,由於須要對頁面進行動態渲染,又不想一直刷新頁面數據,而是讓瀏覽器根據客戶須要動態對渲染數據,這個時候就用到了setInterval這個函數,但是這個函數和setTimeout()相比又有什麼區別呢?咱們知道:javascript

  • setTimeout()是綁定在瀏覽器window的一個方法,能夠經過這個方法指定一段代碼或者函數在多少毫秒(ms)後執行,並返回該定時器的編號,固然了咱們能夠經過clearTimeout()取消代碼的執行
  • setInterval()綁定在瀏覽器window的一個方法,能夠經過setInterval指定一段代碼或函數定時在多少毫秒(ms)後執行,並傳回該定時器的編號,固然了咱們能夠經過clearInterval()取消代碼的執行
    最有效且直接的就是作個小實驗感覺下:

setInterval()執行方法實際上是將需執行的代碼加入到任務隊列,直到輪到代碼執行時,肯定時間是否已經到了,若到達則執行代碼java

var startTime=new Date();
var func = function(){
console.log('start: ' + (new Date()-startTime));
for(var i=0; i<1000000000; i++){}
console.log('end: ' + (new Date()-startTime));
};
setInterval(func,1000);

上面這段代碼的執行以後,你會發現setInterval的end與start時間跳動很是大,並非咱們設置的1000ms。因爲setInterval是一開始就標定了執行的時間點,當所註冊的函數(func)超過,所以不會是固定的1000ms。
setTimeout() 與 setInterval() 的源碼分析
setTimeout()的用法大體與setInterval相同,不一樣的在於定時執行,咱們一樣來測試延遲執行的現象瀏覽器

var startTime=new Date();
var func = function(){
console.log('start: ' + (new Date()-startTime));
for(var i=0; i<1000000000; i++){};
console.log('end: ' + (new Date()-startTime));
setTimeout(func,1000);
};
setTimeout(func,1000);

觀察下圖能夠發現 setTimeout 所設置的時間間隔,會由於目前任務隊列所執行的代碼而可能發生延誤執行的狀況,咱們能夠發現上面這段代碼,執行func的end與start時間間隔基本上是符合咱們所設定的1000ms
setTimeout() 與 setInterval() 的源碼分析
透過現象看本質,咱們不妨看下這兩個函數的源碼
setTimeout() 與 setInterval() 的源碼分析
從函數聲明能夠知道setInterval這裏容許傳參,容許咱們傳入一個方法,讓其在必定時間(number)後執行,其時間等待依賴於調度器_scheduler,而方法的執行是由_fnAndFlush來負責調控的,不過因爲有_requeuePeriodicTimer這個方法的存在使得時間間隔不是咱們所設固定1000msapp

....
case 'setInterval':
  task.data!['handleId'] = this._setInterval(
      task.invoke, task.data!['delay']!,
      Array.prototype.slice.call((task.data as any)['args'], 2));
  break; 
....

private _setInterval(fn: Function, interval: number, args: any[]): number {
  let id = Scheduler.nextId;
  let completers = {onSuccess: null as any, onError: this._dequeuePeriodicTimer(id)};
  let cb = this._fnAndFlush(fn, completers);

  // Use the callback created above to requeue on success.
  completers.onSuccess = this._requeuePeriodicTimer(cb, interval, args, id);

  // Queue the callback and dequeue the periodic timer only on error.
  this._scheduler.scheduleFunction(cb, interval, args, true);
  this.pendingPeriodicTimers.push(id);
  return id;
}

先來看下_fnAndFlush的代碼,這個函數實際上是將咱們的所須要執行的函數刷入內存中,等待瀏覽器的執行ide

private _fnAndFlush(fn: Function, completers: {onSuccess?: Function, onError?: Function}):
    Function {
  return (...args: any[]): boolean => {
    fn.apply(global, args);

    if (this._lastError === null) {  // Success
      if (completers.onSuccess != null) {
        completers.onSuccess.apply(global);
      }
      // Flush microtasks only on success.
      this.flushMicrotasks();
    } else {  // Failure
      if (completers.onError != null) {
        completers.onError.apply(global);
      }
    }
    // Return true if there were no errors, false otherwise.
    return this._lastError === null;
  };
}

咱們不妨來看下_requeuePeriodicTimer這個方法所完成的功能的特色。其會將瀏覽器內存中存在的函數加入隊列中執行(若是存在的話),函數功能完成過程當中已經進入下個scheduler的執行週期,即函數在執行過程當中還有一個計時器在等待下個函數的執行。正如實驗中,咱們觀察到的全部的start間隔大體是1000ms函數

private _requeuePeriodicTimer(fn: Function, interval: number, args: any[], id: number): Function {
  return () => {
    // Requeue the timer callback if it's not been canceled.
    if (this.pendingPeriodicTimers.indexOf(id) !== -1) {
      this._scheduler.scheduleFunction(fn, interval, args, true, false, id);
    }
  };
}

對於setTimeout這裏容許傳參,容許咱們傳入一個方法,讓其在必定時間(number)後執行,其時間等待依賴於調度器_scheduler,而調度器正是讓咱們的函數時間間隔符合咱們設置的1000ms的緣由,而方法的執行則是由_fnAndFlush來負責調控的,由於setTimeout並不會將執行函數推入隊列中,所以計時器不會運行直到前一個週期的函數都執行完畢以後纔開始進入下一個週期,正如實驗代碼中所寫的等待1000ms源碼分析

...
case 'setTimeout':
  task.data!['handleId'] = this._setTimeout(
      task.invoke, task.data!['delay']!,
      Array.prototype.slice.call((task.data as any)['args'], 2));
  break;
...
private _setTimeout(fn: Function, delay: number, args: any[], isTimer = true): number {
  let removeTimerFn = this._dequeueTimer(Scheduler.nextId);
  // Queue the callback and dequeue the timer on success and error.
  let cb = this._fnAndFlush(fn, {onSuccess: removeTimerFn, onError: removeTimerFn});
  let id = this._scheduler.scheduleFunction(cb, delay, args, false, !isTimer);
  if (isTimer) {
    this.pendingTimers.push(id);
  }
  return id;
}

參考資料:
https://www.jeffjade.com/2016/01/10/2016-01-10-javacript-setTimeout/
https://www.jeffjade.com/2016/01/10/2016-01-10-javaScript-setInterval/測試

相關文章
相關標籤/搜索