node - timer學習

以前大概看了libuv的源碼,看到eventloop裏每一個phase,一旦進入以後,在這個phase的回調隊列的全部回調被執行完以前,是不會返回的。這就和node的表現有出入了,node 11以後,每執行一個回調,都會看一看有沒有待執行 nextTick 和 microtask回調。 感受有點詭異,因此就看了一下node這邊timer的源碼。node

流程

先看看暴露給咱用的setTimeout函數:git

function setTimeout(callback, after, arg1, arg2, arg3) {
  if (typeof callback !== 'function') {
    throw new ERR_INVALID_CALLBACK(callback);
  }

  var i, args;
  switch (arguments.length) {
    // fast cases
    case 1:
    case 2:
      break;
    case 3:
      args = [arg1];
      break;
    case 4:
      args = [arg1, arg2];
      break;
    default:
      args = [arg1, arg2, arg3];
      for (i = 5; i < arguments.length; i++) {
        // Extend array dynamically, makes .apply run much faster in v6.0.0
        args[i - 2] = arguments[i];
      }
      break;
  }

  // 初始化一個Timeout對象(雙向鏈表節點)
  const timeout = new Timeout(callback, after, args, false);
  active(timeout); // 插入到對應的鏈表裏

  return timeout;
}
複製代碼

須要注意的是這裏生成了一個Timeout對象,那Timeout長啥樣?github

function Timeout(callback, after, args, isRepeat) {
  after *= 1; // Coalesce to number or NaN
  if (!(after >= 1 && after <= TIMEOUT_MAX)) {
    if (after > TIMEOUT_MAX) {
      process.emitWarning(`${after} does not fit into` +
                          ' a 32-bit signed integer.' +
                          '\nTimeout duration was set to 1.',
                          'TimeoutOverflowWarning');
    }
    after = 1; // Schedule on next tick, follows browser behavior
  }

  this._idleTimeout = after;  // 延遲時間
  this._idlePrev = this;   // 前一個Timeout指針
  this._idleNext = this;   // 後一個Timeout指針
  this._idleStart = null;
  // This must be set to null first to avoid function tracking
  // on the hidden class, revisit in V8 versions after 6.2
  this._onTimeout = null;
  this._onTimeout = callback;  // 傳進來的回調函數
  this._timerArgs = args;
  this._repeat = isRepeat ? after : null;  // 是否須要重複
  this._destroyed = false;

  this[kRefed] = null;

  initAsyncResource(this, 'Timeout');
}
複製代碼

能夠看到,Timeout對象在初始化的時候幹了3件事:bash

  1. 看一看傳進來的延時的參數是否是合規(1 - 2*31 - 1),不合規的話就設成1。 注意,延遲0ms 也是不合法的,也就是說平時咱寫的 setTimout(cb, 0)其實就等於寫了 setTimout(cb, 1)
  2. 初始化一堆本身的屬性,須要注意的是這裏多了一個_idlePrev 和 _idleNext參數,看起來像是個雙向鏈表? 事實就是的, 爲了優化性能, node會根據延時參數建立多個鏈表,每一個Timeout對象都是鏈表的節點,後面還會總結一下。
  3. 初始化async資源,這個就是爲了實現async_hooks的功能了,和timer沒啥關係。

回到setTimeout,在建立好Timeout對象後,就調用了active函數把這個timeout放到了列表裏:app

function active(item) {
  insert(item, true, getLibuvNow());
}
複製代碼

這裏介紹一下node對Timeout的處理,其實 internal/timers.js開頭的註釋也說的很清楚了:async

// Object maps are kept which contain linked lists keyed by their duration in
// milliseconds.
//
/* eslint-disable node-core/non-ascii-character */
//
// ╔════ > Object Map
// ║
// ╠══
// ║ lists: { '40': { }, '320': { etc } } (keys of millisecond duration)
// ╚══          ┌────┘
//              │
// ╔══          │
// ║ TimersList { _idleNext: { }, _idlePrev: (self) }
// ║         ┌────────────────┘
// ║    ╔══  │                              ^
// ║    ║    { _idleNext: { },  _idlePrev: { }, _onTimeout: (callback) }
// ║    ║      ┌───────────┘
// ║    ║      │                                  ^
// ║    ║      { _idleNext: { etc },  _idlePrev: { }, _onTimeout: (callback) }
// ╠══  ╠══
// ║    ║
// ║    ╚════ >  Actual JavaScript timeouts
// ║
// ╚════ > Linked List
//
/* eslint-enable node-core/non-ascii-character */
//
// With this, virtually constant-time insertion (append), removal, and timeout
// is possible in the JavaScript layer. Any one list of timers is able to be
// sorted by just appending to it because all timers within share the same
// duration. Therefore, any timer added later will always have been scheduled to
// timeout later, thus only needing to be appended.
// Removal from an object-property linked list is also virtually constant-time
// as can be seen in the lib/internal/linkedlist.js implementation.
// Timeouts only need to process any timers currently due to expire, which will
// always be at the beginning of the list for reasons stated above. Any timers
// after the first one encountered that does not yet need to timeout will also
// always be due to timeout at a later time.
複製代碼

大概意思就是 根據延時時間的不一樣,好比如今有 m個30ms, n個50ms的setTimeout調用,node就會生成m個Timeout對象組成一個鏈表,放到一個對象裏,key爲30ms,value是這個鏈表。 同理也會生成n個Timeout組成50ms的鏈表。 這麼幹好處是啥呢, 註釋裏也說了,這麼搞的話, Timer的 插入、刪除等操做的時間複雜度都差很少是常量。 想一想也是,你們都是30ms的回調,新來的timer確定比以前就來的更晚過時麼,因此直接塞到隊尾就行了。ide

結構說清楚了再來看看insert函數,函數

function insert(item, refed, start) {
  let msecs = item._idleTimeout;
  if (msecs < 0 || msecs === undefined)
    return;

  // Truncate so that accuracy of sub-milisecond timers is not assumed.
  msecs = Math.trunc(msecs);

  item._idleStart = start;   // 當前事件循環開始的時間,在libuv每一個時間循環開始都會更新一次

  // 看一下有沒有對應的延時鏈表,沒有的話,就再建立一個
  var list = timerListMap[msecs];
  if (list === undefined) {
    debug('no %d list was found in insert, creating a new one', msecs);
    const expiry = start + msecs;
    timerListMap[msecs] = list = new TimersList(expiry, msecs);
    timerListQueue.insert(list);

    // 若是過時時間比以前最近的過時時間還早,那就也schedule一下
    if (nextExpiry > expiry) {
      scheduleTimer(msecs);
      nextExpiry = expiry;
    }
  }

  ......

  // 把Timeout放到鏈表最後面
  L.append(list, item);
}
複製代碼

首先會看一下是否是已經有對應延時的鏈表了,若是沒有,就新建一個。建好以後,直接扔到鏈表的最後。oop

這裏須要注意的是 scheduleTimer, 若是新的Timeout過時時間最近,那就要schedule這個Timeout。性能

再看看scheduleTimer函數,

void Environment::ScheduleTimer(int64_t duration_ms) {
  if (started_cleanup_) return;
  uv_timer_start(timer_handle(), RunTimers, duration_ms, 0);
}
複製代碼

這裏的uv_timer_start函數其實就是往libuv的timer phase註冊了一個回調,這裏咱重點關注一下傳入的回調函數RunTimers,

void Environment::RunTimers(uv_timer_t* handle) {
  .... setup ...

  Local<Function> cb = env->timers_callback_function();
  MaybeLocal<Value> ret;
  Local<Value> arg = env->GetNow();
  /* This code will loop until all currently due timers will    * process. It is impossible for us to end up in an          * infinite loop due to how the JS-side
   * /
  // is structured.
  do {
    TryCatchScope try_catch(env);
    try_catch.SetVerbose(true);
    ret = cb->Call(env->context(), process, 1, &arg);
  } while (ret.IsEmpty() && env->can_call_into_js());

  ......
}
複製代碼

這裏只須要關注到調用了env->timers_callback_function()這個函數,這個函數實際上是經過binding經過processTimers作的封裝,看一看processTimers

function processTimers(now) {
    debug('process timer lists %d', now);
    nextExpiry = Infinity;

    let list;
    let ranAtLeastOneList = false;
    while (list = timerListQueue.peek()) {
      if (list.expiry > now) {
        nextExpiry = list.expiry;
        return refCount > 0 ? nextExpiry : -nextExpiry;
      }
      if (ranAtLeastOneList)
        runNextTicks(); // 執行nextTick回調
      else
        ranAtLeastOneList = true;
      listOnTimeout(list, now);  // 執行一個list裏全部到期的回調
    }
    return 0;
  }
複製代碼

其實就是每次拿出過時時間最近的Timeout,看看時候到時了,到時了的話,就對這個Timeout調用listOnTimeout函數。

function listOnTimeout(list, now) {
    const msecs = list.msecs;

    debug('timeout callback %d', msecs);

    var diff, timer;
    let ranAtLeastOneTimer = false;
    while (timer = L.peek(list)) {
      diff = now - timer._idleStart;

      // Check if this loop iteration is too early for the next timer.
      // This happens if there are more timers scheduled for later in the list.
      if (diff < msecs) {
        list.expiry = Math.max(timer._idleStart + msecs, now + 1);
        list.id = timerListId++;
        timerListQueue.percolateDown(1); // 調整timerListQueue順序,把過時時間最近的放到前面去
        debug('%d list wait because diff is %d', msecs, diff);
        return;
      }

      if (ranAtLeastOneTimer)
        runNextTicks();
      else
        ranAtLeastOneTimer = true;

      // 把timer從列表中刪掉
      L.remove(timer);

      ......

      let start;
      if (timer._repeat)
        start = getLibuvNow();

      try {
        const args = timer._timerArgs;
        // 執行js傳入的回調
        if (args === undefined)
          timer._onTimeout();  
        else
          Reflect.apply(timer._onTimeout, timer, args);
      } finally {
        // 若是是須要重複的timer,從新insert進鏈表裏
        if (timer._repeat && timer._idleTimeout !== -1) {
          timer._idleTimeout = timer._repeat;
          if (start === undefined)
            start = getLibuvNow();
          insert(timer, timer[kRefed], start);
        } else if (!timer._idleNext && !timer._idlePrev) {
          if (timer[kRefed])
            refCount--;
          timer[kRefed] = null;

          if (destroyHooksExist() && !timer._destroyed) {
            emitDestroy(timer[async_id_symbol]);
            timer._destroyed = true;
          }
        }
      }

      emitAfter(asyncId);
    }
    ....
  }
複製代碼

能夠看到,和以前的邏輯差很少,就在鏈表裏一直拿出Timeout,直到拿出的Timeout還沒到過時時間,這個時候就把當前的list,在timerListQueue日後移動。 好比timerListQueue裏面原本按次序放着30ms, 50ms,70ms 3個list, 這裏就會先看30ms的list,有哪些Timeout過時了的就執行掉,而後會再看50ms和70ms的,這裏看完以後,這個時間循環階段全部的timer也就都看完了。

須要特別注意的是,在看一看processTimers和listOnTimeout裏,若是不是第一次執行,都會先調用runNextTicks這個函數,看名字就知道是幹啥的了,執行nextTick回調

這個函數實際上是封裝了processTicksAndRejections,

function processTicksAndRejections() {
  let tock;
  do {
    while (tock = queue.shift()) {
      const asyncId = tock[async_id_symbol];
      emitBefore(asyncId, tock[trigger_async_id_symbol]);
      
      if (destroyHooksExist())
        emitDestroy(asyncId);

      const callback = tock.callback;
      if (tock.args === undefined)
        callback();
      else
        Reflect.apply(callback, undefined, tock.args);

      emitAfter(asyncId);
    }
    setHasTickScheduled(false);
    runMicrotasks();
  } while (!queue.isEmpty() || processPromiseRejections());
  setHasRejectionToWarn(false);
}
複製代碼

能夠看到,每次執行完nextTickQueue裏的回調後,會調用runMicroTasks這個函數,這函數就是去執行v8的微任務隊列。

看到這裏,一開始的疑惑基本就解開了。node是把咱傳給setTimeout的回調給封裝了一下(processTimers), 封裝裏面,調用實際傳入的回調前,都會先去檢查一下nextTickQueue和microTaskQueue, 若是裏面有回調的話,得先執行完。

參考

  1. New Changes to the Timers and Microtasks in Node v11.0.0 ( and above)
  2. node source code
  3. libuv source code
相關文章
相關標籤/搜索