关于setTimeout计时器占用空间的探究

1,114 阅读4分钟

关于setTimeout计时器占用空间的探究

问题的起因:

我直觉上自然是觉得,肯定是会销毁的,但是出于科学严谨的态度,我需要更加深入地了解setTime函数的机制,再来回答这个问题。

下文中,不论setTimeout还是setTimeInterval我们都先统称为setTime函数。

浏览器中

img

首先,通过查阅MDN(链接)可以得知,在浏览器环境中,setTime函数返回的都是一个定时器的编号,也即一个number,所以,对应的,将其设为null没有任何意义,只有通过clear函数,才能将定时器从某个挂载的对象上取消。

由于不知道如何探究浏览器环境js内置函数实现,暂且只能探索到这个地步。

nodejs中

由于node是开源的,我们可以直接通过查看源码的实现来分析这个问题。

在node的lib目录下的timers.js文件中,我们可以可以看到setTimeout函数的定义

// lib>timers.js>setTimeout
function setTimeout(callback, after, arg1, arg2, arg3) {
  validateCallback(callback);

  let 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;
  }

  const timeout = new Timeout(callback, after, args, false, true);
  insert(timeout, timeout._idleTimeout);

  return timeout;
}


//lib>internal>timers.js>Timeout
function Timeout(callback, after, args, isRepeat, isRefed) {
  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;
  this._idleNext = this;
  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;

  if (isRefed)
    incRefCount();
  this[kRefed] = isRefed;
  this[kHasPrimitive] = false;

  initAsyncResource(this, 'Timeout');
}

可以发现与浏览器环境中不同,在node中,setTimeout函数返回的是一个Timeout对象。通过查看Timeout对象的定义,可以发现_idleTimeout属性记录了定时器将在多久之后执行callback。由此,可以猜测,通过insert(timeout, timeout._idleTimeout);语句,timeout对象将会被插入到一个存储对应时间timer的结构中。

// Object map containing linked lists of timers, keyed and sorted by their
// duration in milliseconds.
//
// - key = time in milliseconds
// - value = linked list
const timerListMap = ObjectCreate(null);

function insert(item, msecs, start = getLibuvNow()) {
  // Truncate so that accuracy of sub-millisecond timers is not assumed.
  msecs = MathTrunc(msecs);
  item._idleStart = start;

  // Use an existing list if there is one, otherwise we need to make a new one.
  let 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);

    if (nextExpiry > expiry) {
      scheduleTimer(msecs);
      nextExpiry = expiry;
    }
  }

  L.append(list, item);
}

由上代码可以发现,事实的确如此,通过timerListMap[msecs]存储msecs毫秒后需要执行的timer(值得一提的是其中TimersList为链表结构)。对应的可以找到,timer在node中被手动删除和执行后自动移除相关的源码。

//lib>timers.js


function clearTimeout(timer) {
  if (timer && timer._onTimeout) {
    timer._onTimeout = null;
    unenroll(timer);
    return;
  }
  if (typeof timer === 'number' || typeof timer === 'string') {
    const timerInstance = knownTimersById[timer];
    if (timerInstance !== undefined) {
      timerInstance._onTimeout = null;
      unenroll(timerInstance);
    }
  }
}

function unenroll(item) {
  if (item._destroyed)
    return;

  item._destroyed = true;

  if (item[kHasPrimitive])
    delete knownTimersById[item[async_id_symbol]];

  if (destroyHooksExist() && item[async_id_symbol] !== undefined)
    emitDestroy(item[async_id_symbol]);

  L.remove(item);

  
  if (item[kRefed]) {
    const msecs = MathTrunc(item._idleTimeout);
    const list = timerListMap[msecs];
    if (list !== undefined && L.isEmpty(list)) {
      debug('unenroll: list empty');
      timerListQueue.removeAt(list.priorityQueuePosition);
      delete timerListMap[list.msecs];
    }

    decRefCount();
 }
    
 //lib>internal>timer.js >getTimerCallbacks>listOnTimeout
 
 function listOnTimeout(list, now) {
    const msecs = list.msecs;

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

    let ranAtLeastOneTimer = false;
    let timer;
    while (timer = L.peek(list)) {
      const 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 = MathMax(timer._idleStart + msecs, now + 1);
        list.id = timerListId++;
        timerListQueue.percolateDown(1);
        debug('%d list wait because diff is %d', msecs, diff);
        return;
      }

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

      // The actual logic for when a timeout happens.
      L.remove(timer);

      const asyncId = timer[async_id_symbol];

      if (!timer._onTimeout) {
        if (!timer._destroyed) {
          timer._destroyed = true;

          if (timer[kRefed])
            refCount--;

          if (destroyHooksExist())
            emitDestroy(asyncId);
        }
        continue;
      }

      emitBefore(asyncId, timer[trigger_async_id_symbol], timer);

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

      try {
        const args = timer._timerArgs;
        if (args === undefined)
          timer._onTimeout();
        else
          ReflectApply(timer._onTimeout, timer, args);
      } finally {
        if (timer._repeat && timer._idleTimeout !== -1) {
          timer._idleTimeout = timer._repeat;
          insert(timer, timer._idleTimeout, start);
        } else if (!timer._idleNext && !timer._idlePrev && !timer._destroyed) {
          timer._destroyed = true;

          if (timer[kRefed])
            refCount--;

          if (destroyHooksExist())
            emitDestroy(asyncId);
        }
      }

      emitAfter(asyncId);
    }

    // If `L.peek(list)` returned nothing, the list was either empty or we have
    // called all of the timer timeouts.
    // As such, we can remove the list from the object map and
    // the PriorityQueue.
    debug('%d list empty', msecs);

    // The current list may have been removed and recreated since the reference
    // to `list` was created. Make sure they're the same instance of the list
    // before destroying.
    if (list === timerListMap[msecs]) {
      delete timerListMap[msecs];
      timerListQueue.shift();
    }
  }

可以看到在clearTimeout函数中,在手动取消timer之后,会再通过unenroll函数从对应的结构中删除去对用的timer,并相应地消去影响,而在listOnTimeout函数中,会对对应时刻的timerList进行遍历,每扫到一个timer就将其从list中移除,由此可以确定,对于setTimeout函数,即使不手动clearTimeout,计时器也会在任务执行完成后自动销毁。同时,可以发现其实setInterval的timer由于存在repeat属性,所以在被从队列中移除了之后还会重新被insert到对应的新时刻。