如何防止从树中删除的 DOM 节点被虚假的强引用(例如闭包)所持有?

问题描述 投票:0回答:1

对于一个玩具示例,假设我有一个时钟小部件:

{
  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) 中都会发生。

这是浏览器的缺陷吗?或者我是否错过了其他一些强有力的参考?

javascript dom closures weak-references
1个回答
0
投票

当然,如果代码不依赖 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;
    });
}
© www.soinside.com 2019 - 2024. All rights reserved.