如果事件处理闭包引用使用键控数据连接的 d3.js 选择,它们似乎会导致 DOM 节点泄漏。
为什么会这样?是 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()
}
此模式可通过 D3.js 3.5.3 重现并可在 Chrome 39 中识别。
当满足两个条件时,DOM 节点似乎会泄漏:
- 选择有关键作用
- 一个闭包,用作选择中的一个节点的事件处理程序,具有对外部范围选择的引用。不必执行闭包。
这些步骤中的任何一个都可以防止内存泄漏:
- 在调用
data
时不使用键函数 - 在
step
的末尾添加listItems = null
- 避免在闭包中引用外部选择
- 在点击处理闭包中添加
listItems = null
。
后一点特别有趣,因为它释放了所有 泄漏的节点,而不仅仅是当前listItems
选择中的节点。这意味着选择是链接的,这是我没想到的。
检查 Chrome DevTools 中的堆快照显示泄漏的 HTMLLIElement
对象在其保留器层次结构中有两个不同的 listItems
:
这是预期的行为吗?如果是这样,是什么原因造成的?这是我的代码或 d3.js 中的内存泄漏吗?
最佳答案
在添加新元素的进入阶段,您将绑定(bind)到每个新添加的“li”元素的 onClick 处理程序。
listItems.enter()
.append('li')
.text(function(d) { return '#' + d })
.on('click', clickHandler);
在退出阶段,您将删除不再需要的“li”元素。但是,在删除“li”元素之前,您并未解除与 onClick 处理程序的绑定(bind)。
在您发布的分析器图像中,请注意 HTMLLIElement 是红色的。 Chrome 的内存分析器告诉您 HTMLIElement 已与 DOM 树断开连接,但仍有对它的 javascript 引用。在这种情况下,“li”元素的 onClick 处理程序引用了您的 js 代码。
在 D3 的退出阶段通过调用 .on('click',null)
移除点击处理程序。
listItems.exit()
.on('click', null)
.remove();
将删除对您的 clickHandler 的引用。
关于javascript - 事件处理程序关闭中 d3.js 键控连接的内存泄漏,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27864091/