关于setTimeout计时器占用空间的探究
问题的起因:
我直觉上自然是觉得,肯定是会销毁的,但是出于科学严谨的态度,我需要更加深入地了解setTime函数的机制,再来回答这个问题。
下文中,不论setTimeout还是setTimeInterval我们都先统称为setTime函数。
浏览器中
首先,通过查阅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到对应的新时刻。