对于一个玩具示例,假设我有一个时钟小部件:
{
const clockElem = document.getElementById('clock');
const timefmt = new Intl.DateTimeFormat(
'default', { timeStyle: 'medium', });
setInterval(() => {
const d = new Date;
console.log('tick', d);
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>
当我单击按钮删除时钟时,仍会调用
setInterval
回调。回调闭包强力持有 DOM 节点,这意味着它的资源无法被释放。还有来自按钮事件处理程序的循环引用;尽管也许这个问题可以由发动机的循环收集器来处理。虽然也许不是。
永远不要害怕:我可以创建一个辅助函数,确保闭包仅通过弱引用保存 DOM 节点。
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 clockElem = document.getElementById('clock');
const timefmt = new Intl.DateTimeFormat(
'default', { timeStyle: 'medium', });
setInterval(weakCapture([clockElem], ([clockElem]) => {
const d = new Date;
console.log('tick', d);
clockElem.querySelector('p').innerHTML =
timefmt.format(d);
}), 1000);
clockElem.querySelector('button')
.addEventListener('click',
weakCapture([clockElem], ([clockElem], ev) => {
clockElem.remove();
}));
}
<div id="clock">
<button>Remove</button>
<p></p>
</div>
但这似乎不起作用。即使在删除
clockElem
节点后,“tick”仍会继续记录到控制台,这意味着 WeakRef
尚未被清空,这意味着某些东西似乎仍然对 clockElem
有很强的引用。鉴于 GC 不能保证立即运行,当然,我预计会有一些延迟,但即使当我尝试通过在控制台中运行像 +'9'.repeat(1e9)
这样占用大量内存的代码来强制 GC 时,弱引用也不会被清除(尽管这是足以在更琐碎的情况下强制 GC 并清除弱引用,例如 new WeakRef({})
)。这种情况在 Chromium (118.0.5993.117) 和 Firefox (115.3.0esr) 中都会发生。
这是浏览器的缺陷吗?或者我是否错过了其他一些强有力的参考?
当然,如果代码不依赖 d 和 timefmt 来生存,则可以将它们作为参数传递给 setInterval 而不是依赖weakCapture:
{
const clockElem = document.getElementById('clock');
const timefmt = new Intl.DateTimeFormat(
'default', { timeStyle: 'medium', });
let cancelled = false;
clockElem.data = { timefmt, cancelled };
setInterval(() => {
if (!clockElem.data.cancelled) {
const d = new Date;
clockElem.querySelector('p').innerHTML =
clockElem.data.timefmt.format(d);
}
}, 1000);
clockElem.querySelector('button')
.addEventListener('click', ev => {
clockElem.remove();
cancelled = true;
});
}
如果有其他代码需要 dom 节点和这些值,我们可以将一个弱数据对象传递给 setInterval 并在每次更改时更新它保存的值,再次使用weakCapture。即使假设浏览器没有显式循环收集器,循环引用也没关系,因为毕竟常规垃圾收集可以管理循环引用。
{
const clockElem = document.getElementById('clock');
let timefmt = new Intl.DateTimeFormat(
'default', { timeStyle: 'medium', });
let cancelled = false;
const update = weakCapture([clockElem, clockElem.data],
([clockElem, data]) => {
const d = new Date;
clockElem.querySelector('p')
.innerHTML = data.timefmt.format(d);
});
setInterval(update, 1000);
clockElem.querySelector('button')
.addEventListener('click', ev => {
clockElem.remove();
cancelled = true;
});
}
或者,只需设置显式回调而不是使用 setInterval。
{
const clockElem = document.getElementById('clock');
const setUpdate = weakCapture([clockElem], ([clockElem]) => {
const d = new Date;
clockElem.querySelector('p').innerHTML =
timefmt.format(d);
});
let cancelled = false;
clockElem.data = { setUpdate, cancelled };
const update = () => {
if (!clockElem.data.cancelled) {
const d = new Date;
clockElem.data.setUpdate();
setTimeout(update, 1000 - d.getMilliseconds());
}
};
update();
clockElem.querySelector('button')
.addEventListener('click', ev => {
clockElem.remove();
clockElem.data.cancelled = true;
});
}