提问人:user3840170 提问时间:10/31/2023 最后编辑:user3840170 更新时间:11/11/2023 访问量:310
如何防止从树中删除的 DOM 节点被虚假的强引用(例如闭包)持有?
How can I prevent a DOM node removed from its tree from being held by spurious strong references, like from closures?
问:
举个玩具的例子,假设我有一个时钟小部件:
{
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>
当我单击按钮删除时钟时,仍然会调用回调。回调闭包牢固地保留了 DOM 节点,这意味着其资源无法被释放。还有来自按钮事件处理程序的循环引用;尽管也许可以由发动机的循环收集器处理。话又说回来,也许不是。setInterval
不用担心:我可以创建一个辅助函数,确保闭包仅通过弱引用来保存 DOM 节点,并投入以清理计时器。FinalizationRegistry
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>
但这似乎行不通。即使在删除节点后,“tick”仍会继续记录到控制台中,这意味着尚未清空,这意味着某些内容似乎仍然具有对 的强引用。鉴于 GC 不能保证立即运行,我当然预计会有一些延迟,但即使我尝试通过运行内存密集型代码(如在控制台中)来强制 GC,弱引用也不会被清除(尽管这足以强制 GC 并在更微不足道的情况下清除弱引用,例如)。这在 Chromium (118.0.5993.117) 和 Firefox (115.3.0esr) 中都会发生。clockElem
WeakRef
clockElem
+'9'.repeat(1e9)
new WeakRef({})
这是浏览器中的缺陷吗?或者我错过了其他一些强有力的参考?
(简而言之:这是在 JavaScript 中实现弱事件模式的尝试。
答:
[Element].remove()
从 DOM 中删除(“断开连接”),但不从内存中删除。只要没有结束,它就会继续愉快地运行,从内存中取出元素。Element
setInterval
因此,结束 timer 函数将使元素可进行垃圾回收(当然,如果没有其他引用)。
您可以使用其 isConnected 属性检查 DOM 中是否存在元素,如果该元素不再连接,则结束计时器。
对于时钟示例,例如(更改为更易于管理)。setInterval
setTimeout
document.addEventListener('click', ev =>
ev.target.closest(`#clock`)?.remove());
let timer;
const log = t => document.querySelector(`#log2Screen`).textContent = t;
const clockElem = document.getElementById('clock');
const timefmt = new Intl.DateTimeFormat('default', { timeStyle: 'medium', });
run();
function run() {
if (!clockElem.isConnected) {
console.log(
`div#clock exists in memory:`,
clockElem );
// clear timer when #clock not in DOM
return clearTimeout(timer);
}
const d = timefmt.format(new Date);
log(`tick ${d}`);
clockElem.querySelector('p').textContent = d;
timer = setTimeout(run, 1000);
}
#log2Screen {
color: green;
}
<div id="clock">
<button id="remove">Remove</button>
<p></p>
</div>
<div id="log2Screen"></div>
备选方案 1:从 DOM 中删除元素时,可以结束计时器(以此类推,删除 )。div#clock
let timer;
document.addEventListener('click', ev => {
ev.target.closest(`#clock`)?.remove();
clearTimeout(timer); // <= clear timer
} );
const log = t => document.querySelector(`#log2Screen`).textContent = t;
const clockElem = document.getElementById('clock');
const timefmt = new Intl.DateTimeFormat('default', { timeStyle: 'medium', });
run();
function run() {
const d = timefmt.format(new Date);
log(`tick ${d}`);
clockElem.querySelector('p').textContent = d;
timer = setTimeout(run, 1000);
}
#log2Screen {
color: green;
}
<div id="clock">
<button id="remove">Remove</button>
<p></p>
</div>
<div id="log2Screen"></div>
备选方案 2:您可以将元素的赋值定位在定时器函数中,并且仅在仍连接到 DOM 时才继续定时器。#clock
div#clock
document.addEventListener('click', ev => {
ev.target.closest(`#clock`)?.remove();
} );
let [timer, tick] = [, 0];
const log = t => document.querySelector(`#log2Screen`).textContent = t;
const timefmt = new Intl.DateTimeFormat('default', { timeStyle: 'medium', });
run();
function run() {
const clockEl = document.querySelector('#clock p');
if (clockEl) { // <= only run when #clock in DOM
log(`tick ${++tick}`);
clockEl.textContent = timefmt.format(new Date);
return timer = setTimeout(run, 1000);
}
clearTimeout(timer);
log(`Clock deactived on ${timefmt.format(new Date)}`);
}
#log2Screen {
color: green;
}
<div id="clock">
<button id="remove">Remove</button>
<p></p>
</div>
<div id="log2Screen"></div>
这里用于删除节点的回调可用于清除声明时存储的间隔。
{
const clockElem = document.getElementById('clock');
const timefmt = new Intl.DateTimeFormat(
'default', { timeStyle: 'medium', });
const interval = setInterval(() => {
const d = new Date;
console.log('tick', d, clockElem);
clockElem.querySelector('p').innerHTML =
timefmt.format(d);
}, 1000);
clockElem.querySelector('button')
.addEventListener('click', ev => {
clearInterval(interval);
clockElem.remove();
});
}
<div id="clock">
<button>Remove</button>
<p></p>
</div>
评论
FinalisationRegistry
评论
timefmt
clockElem
weakCapture([clockElem, timefmt], ([clockElem, timefmt]) => {
clockElem = null
let
wr = new WeakRef({}); await +'9'.repeat(1e9); wr.deref()
about:memory