事件处理程序关闭中 d3.js 键控连接的内存泄漏

Memory leak with d3.js keyed join in event handler closure

似乎 event-handling 闭包可能导致 DOM 节点泄漏,如果它们引用使用键控数据连接的 d3.js 选择。

为什么会这样? d3.js 或调用方式有问题吗?

此示例在重复调用 step 时泄漏 HTMLLIElement 个对象(不必执行 clickHandler):

function getKeys(n) {
  // returns a random array of n unique Strings, e.g. ["Alpha", "Quebec", "Charlie"]
}

function step() {

  function clickHandler() {
    // removing this reference removes the leak
    // (note that the outer variable is pulled into closure scope regardless of whether this function is called).
    listItems;
  }

  var keys = getKeys(3);

  var listItems = d3.selectAll('li')
    .data(keys,  function(d) { return d }); 

  listItems.enter()
    .append('li')
    .text(function(d) { return '#' + d })
    .on('click', clickHandler)

  listItems.exit()
    .remove()
}

JSBin

DevTools-friendly version

此模式可在 D3.js 3.5.3 中重现,并可在 Chrome 39 中识别。

似乎 DOM 个节点在满足两个条件时泄漏:

  1. 选择有按键功能
  2. 用作选择中节点之一的事件处理程序的闭包具有对外部范围选择的引用。不必执行闭包。

这些步骤中的任何一个都可以防止内存泄漏:

后一点特别有趣,因为它释放了 所有 的泄漏节点,而不仅仅是当前 listItems 选择中的节点。这意味着选择是链接的,这是我没想到的。

在 Chrome DevTools 中检查堆快照显示泄漏的 HTMLLIElement 对象在其保留层次结构中有两个不同的 listItems

这是预期的行为吗?如果是这样,是什么原因造成的?这是我的代码中的内存泄漏还是 d3.js?

在添加新元素的进入阶段,您将绑定到每个新添加的 'li' 元素的 onClick 处理程序。

listItems.enter()
  .append('li')
  .text(function(d) { return '#' + d })
  .on('click', clickHandler);

在退出阶段,您将删除不再需要的 'li' 元素。但是,在删除 'li' 元素之前,您并未解除与 onClick 处理程序的绑定。

在您发布的分析器图像中,请注意 HTMLLIElement 是红色的。 Chrome 的内存分析器告诉您 HTMLIElement 已与 DOM 树断开连接,但仍有 javascript 对它的引用。在这种情况下,'li' 元素的 onClick 处理程序引用了您的 js 代码。

通过在 D3 的退出阶段调用 .on('click',null) 删除点击处理程序。

listItems.exit()
  .on('click', null)
  .remove();

将删除对您的 clickHandler 的引用。

很有趣。我发现了由 d3.transition() 引起的内存泄漏。基本上发生的事情是图表在一段时间不活动后挂起 - 首先更新停止,然后整个选项卡崩溃,图表完全消失,而任务管理器显示它的大量内存使用,增长缓慢。删除转换修复了所有提到的问题。