For a toy example, suppose I have a clock widget:
{
const clockElem = document.getElementById('clock');
const timefmt = new Intl.DateTimeFormat(
'default', { timeStyle: 'medium', });
setInterval(() => {
const d = new Date;
console.log('tick', d, clockElem);
clockElem.querySelector('p').innerHTML =
timefmt.format(d);
}, 1000);
clockElem.querySelector('button')
.addEventListener('click', ev => {
clockElem.remove();
});
}
<div id="clock">
<button>Remove</button>
<p></p>
</div>
When I click the button to remove the clock, the setInterval callback is still invoked. The callback closure holds the DOM node strongly, which means its resources cannot be freed. There is also the circular reference from the button event handler; though perhaps that one could be handled by the engine’s cycle collector. Then again, maybe not.
Never fear: I can create a helper function ensuring that closures only hold the DOM node by a weak reference, and throw in FinalizationRegistry to clean up the timer.
const weakCapture = (captures, func) => {
captures = captures.map(o => new WeakRef(o));
return (...args) => {
const objs = [];
for (const wr of captures) {
const o = wr.deref();
if (o === void 0)
return;
objs.push(o);
}
return func(objs, ...args);
}
};
const finregTimer = new FinalizationRegistry(
timerId => clearInterval(timerId));
{
let clockElem = document.getElementById('clock');
const timefmt = new Intl.DateTimeFormat(
'default', { timeStyle: 'medium', });
const timerId = setInterval(
weakCapture([clockElem], ([clockElem]) => {
const d = new Date;
console.log('tick', d);
clockElem.querySelector('p').innerHTML =
timefmt.format(d);
}), 1000);
finregTimer.register(clockElem, timerId);
clockElem.querySelector('button')
.addEventListener('click',
weakCapture([clockElem], ([clockElem], ev) => {
clockElem.remove();
}));
clockElem = null;
// now clockElem should be held strongly only by the DOM
}
<div id="clock">
<button>Remove</button>
<p></p>
</div>
<button onclick="+'9'.repeat(1e9)">Try to force GC</button>
But this doesn’t seem to work. Even after the clockElem node is removed, the ‘tick’ keeps being logged to the console, meaning the WeakRef has not been emptied, meaning something seems to still hold a strong reference to clockElem. Given that GC is not guaranteed to run immediately, I expected some delay, of course, but even when I try to force GC by running memory-heavy code like +'9'.repeat(1e9) in the console, the weak reference is not cleared (despite this being enough to force GC and clear weak references in even more trivial cases like new WeakRef({})). This happens both in Chromium (118.0.5993.117) and in Firefox (115.3.0esr).
Is this a flaw in the browsers? Or is there perhaps some other strong reference that I missed?
(In short: this is an attempt to implement the weak event pattern in JavaScript.)
[Element].remove()removes ('disconnects')Elementfrom the DOM, but not from memory. As long assetIntervalis not ended, it will keep running happily with the element from memory.So ending the timer function will make the element garbage collectable (if there are no other references to it ofcourse).
You can check the existence of an element in the DOM using its isConnected property and end the timer if it's not connected anymore.
For the clock example something like (changed
setIntervalto a more manageablesetTimeout).Alternative 1: you can end the timer when removing the element from DOM (so on removing
div#clock).Alternative 2: you can position the assignment of the
#clockelement within the timer function and only continue the timer ifdiv#clockis still connected to the DOM.