问题 如何避免jQuery的内存泄漏?


jQuery在其内部缓存中保存对DOM节点的引用,直到我显式调用$ .remove()。如果我使用像React这样自己删除DOM节点的框架(使用本机DOM元素API),我该如何清理jQuery的mem缓存?

我正在使用React设计一个相当大的应用程序。对于那些不熟悉的人,React将根据自己的“影子”DOM表示拆除DOM并根据需要重建。该部件工作良好,没有内存泄漏。

Flash向前,我们决定使用jQuery插件。在React运行其渲染循环并构建DOM之后,我们初始化插件,这会导致jQuery保存对相应DOM节点的引用。稍后,用户更改页面上的选项卡,React将删除这些DOM元素。不幸的是,因为React不使用jQuery的$ .remove()方法,所以jQuery维护对这些DOM元素的引用,垃圾收集器永远不会清除它们。

有没有办法告诉jQuery刷新缓存,或者更好的是,根本不缓存?我希望仍然能够利用jQuery的插件和跨浏览器的优点。


6582
2018-06-11 23:23


起源

所以,当你说“导航”时,你并不是在谈论实际导航到新页面,对吧?这些是对当前页面进行的DOM更改?
正确。我也会澄清原来的问题。在我的React应用程序中导航意味着基于某些AJAX结果的DOM转换。 - Brent Traut
缓存刷新是否需要仅针对页面的特定区域?如果是这样,您可以在ReactJS进行更改之前对该区域执行操作吗?
据我所知,缓存刷新可以根据需要广泛使用。我个人不需要jQuery来保存我的DOM上的任何状态,所以我可以完全禁用它的内部缓存。我只是不确定它实际上是如何融入框架的。至于React - 我不知道React什么时候会拆除并重建某些元素。我们的想法是使您的组件完全无状态,以便您可以随时重建它们,然后将所有DOM控件放弃到React框架。 - Brent Traut
@ BrentTraut - 听起来你需要分析你使用jQuery的内容并开始替换它。浏览器现在非常一致。 jQuery是一个通用库,必须尝试解决浏览器各个方面的每个错误和不一致问题。实际上很少有Web应用程序需要这样做,有一些简单的策略可以避免大多数浏览器怪癖,因此您可以编写更简单,更清晰的代码。 - RobG


答案:


如果你的插件公开了一种方法来以编程方式销毁其中一个实例(即 $(element).plugin('destroy')),你应该在那里打电话 componentWillUnmount 组件的生命周期。

componentWillUnmount 在从DOM卸载组件之前调用它,它是清除组件在其生命周期中可能创建的所有外部引用/事件侦听器/ dom元素的正确位置。

var MyComponent = React.createClass({
    componentDidMount() {
        $(React.findDOMNode(this.refs.jqueryPluginContainer)).plugin();
    },
    componentWillUnmount() {
        $(React.findDOMNode(this.refs.jqueryPluginContainer)).plugin('destroy');
    },
    render() {
        return <div ref="jqueryPluginContainer" />;
    },
});

如果你的插件没有暴露出自己清理的方法, 本文 列出了一些方法,您可以尝试取消引用一个经过深思熟虑的插件。

但是,如果你是 创建 在你的React组件中使用jQuery的DOM元素,然后你做了一些严重的错误:你应该差不多 决不 在使用React时需要jQuery,因为它已经抽象出了使用DOM的所有痛点。

我也要警惕使用refs。实际上只需要很少的用例,而这些用例通常涉及与操作/读取DOM的第三方库集成。


如果组件有条件地呈现受jQuery插件影响的元素,则可以使用回调引用来侦听其mount / unmount事件。

之前的代码将成为:

var MyComponent = React.createClass({
    handlePluginContainerLifecycle(component) {
        if (component) {
            // plugin container mounted
            this.pluginContainerNode = React.findDOMNode(component);
            $(this.pluginContainerNode).plugin();
        } else {
            // plugin container unmounted
            $(this.pluginContainerNode).plugin('destroy');
        }
    },
    render() {
        return (
            <div>
                {Math.random() > 0.5 &&
                    // conditionally render the element
                    <div ref={this.handlePluginContainerLifecycle} />
                }
            </div>
        );
    },
});

3
2018-06-12 09:34



我没有合作过 应对 但是现在我用Angular来做,你给了一个好点: 你在使用X时几乎不需要jQuery ... 当X是一个与DOM分离的库时。 - jherax
使用jQuery的.remove()销毁插件甚至DOM元素都可以完成 componentWillUnmount,但在组件的生命周期中仍然有很多机会拥有 render 方法拆除并重建DOM。 componentWillUnmount 只涵盖了 持续 一。 - Brent Traut
@BrentTraut好点。我已更新我的答案以涵盖此案例。 - Alexandre Kirszenberg


jQuery的 通过以下方式跟踪事件和其他类型的数据 内部 API jQuery._data() 但由于这种方法是内部的,因此没有官方支持。

内部方法具有以下签名:

jQuery._data( DOMElement, data)

因此,例如,我们将检索附加到Element的所有事件处理程序(通过jQuery):

var allEvents = jQuery._data( document, 'events');

这返回和 Object 含有 事件类型 作为关键,和 一系列事件处理程序 作为价值。

现在,如果您想获取特定类型的所有事件处理程序,我们可以编写如下:

var clickHandlers = (jQuery._data(document, 'events') || {}).click;

这会返回一个 Array 的 “点击”事件处理程序 要么 undefined 如果指定的事件未绑定到Element。

为什么我说这个方法? 因为它允许我们追踪 事件授权 和事件监听器直接连接,以便我们可以找出是否 事件处理程序被绑定了好几次 导致相同的元素 内存泄漏

但是如果你还想要一个没有jQuery的类似功能,你可以用这个方法实现它 getEventHandlers

看看这篇有用的文章:


调试

我们将编写一个简单的函数来打印事件处理程序及其命名空间(如果已指定)

function writeEventHandlers (dom, event) {
    jQuery._data(dom, 'events')[event].forEach(function (item) {
        console.info(new Array(40).join("-"));
        console.log("%cnamespace: " + item.namespace, "color:orangered");
        console.log(item.handler.toString());
    });
}

使用此功能非常简单:

writeEventHandlers(window, "resize");

我写了一些实用程序,允许我们跟踪绑定到DOM Elements的事件

如果您关心性能,您会发现以下链接很有用:

我鼓励任何阅读这篇文章的人,在我们的代码中注意内存分配,因为三个重要的事情我学习了性能问题:

  1. 记忆
  2. 记忆
  3. 是的,记忆。

事件:良好做法

创建命名函数是个好主意 捆绑 和 解除绑定  事件处理程序 来自DOM元素。

如果要动态创建DOM元素,例如,向某些事件添加处理程序,则可以考虑使用 事件授权 而不是将事件侦听器直接绑定到每个元素,这样,动态添加元素的父级将处理该事件。此外,如果您使用jQuery,您可以命名事件;)

//the worse!
$(".my-elements").click(function(){});

//not good, anonymous function can not be unbinded
$(".my-element").on("click", function(){});

//better, named function can be unbinded
$(".my-element").on("click", onClickHandler);
$(".my-element").off("click", onClickHandler);

//delegate! it is bound just one time to a parent element
$("#wrapper").on("click.nsFeature", ".my-elements", onClickMyElement);

//ensure the event handler is not bound several times
$("#wrapper")
    .off(".nsFeature1 .nsFeature2") //unbind event handlers by namespace
    .on("click.nsFeature1", ".show-popup", onShowPopup)
    .on("click.nsFeature2", ".show-tooltip", onShowTooltip);

循环参考

虽然 循环引用 对于那些实现它的浏览器来说不再是问题 标记和扫描算法 在他们的 垃圾收集器如果我们正在交换数据,那么使用这种对象不是明智的做法,因为不可能(现在)序列化为JSON,但在将来的版本中,由于处理这种对象的新算法,它是可能的。 。我们来看一个例子:

var o1 = {};
    o2 = {};
o1.a = o2; // o1 references o2
o2.a = o1; // o2 references o1

//now we try to serialize to JSON
var json = JSON.stringify(o1);
//we get:"Uncaught TypeError: Converting circular structure to JSON"

现在让我们尝试另一个例子

var freeman = {
    name: "Gordon Freeman",
    friends: ["Barney Calhoun"]
};

var david = {
    name: "David Rivera",
    friends: ["John Carmack"]
};

//we create a circular reference
freeman.friends.push(david); //freeman references david
david.friends.push(freeman); //david references freeman

//now we try to serialize to JSON
var json = JSON.stringify(freeman);
//we get:"Uncaught TypeError: Converting circular structure to JSON"

PD:这篇文章是关于 在JavaScript中克隆对象。此要点还包含有关使用循环引用克隆对象的演示: clone.js


重用对象

让我们遵循一些编程原则,   (不要重复自己) 而不是创建具有类似功能的新对象,我们可以用一种奇特的方式抽象它们。在这个例子中,我将重用一个事件处理程序(再次使用事件)

//the usual way
function onShowContainer(e) {
    $("#container").show();
}
function onHideContainer(e) {
    $("#container").hide();
}
$("#btn1").on("click.btn1", onShowContainer);
$("#btn2").on("click.btn2", onHideContainer);
//the good way, passing data to events
function onToggleContainer(e) {
    $("#container").toggle(e.data.show);
}
$("#btn1").on("click.btn1", { show: true }, onToggleContainer);
$("#btn2").on("click.btn2", { show: false }, onToggleContainer);

并且有很多方法可以做到 提高 我们的代码,对我们有影响 性能,并防止 内存泄漏。在这篇文章中,我主要谈到了 事件,但还有其他方法可以产生内存泄漏。我建议阅读之前发布的文章。


快乐阅读和快乐编码!


11
2018-06-12 03:27



你应该提到这一点 $._data() 技术正在使用不受支持的方法。它可能随时破裂。此外,绑定所有处理程序的性能开销也是如此 document。我当然不会称之为“更好”。他们摆脱了这个原因是有原因的 .live() 方法,就是这样做的。
“...避免循环引用,它不允许垃圾收集器恢复分配的内存......” 这只是错误的。垃圾收集器的重点在于它 不 处理循环引用之类的东西。您链接的文章正在讨论一个主要影响旧IE的不同问题。
在第一个评论中你有理由,事件冒泡到达文档。在第二条评论中,我不是在谈论旧的IE,但你又有理由,因为所有现代浏览器都可以处理循环引用。我将更新帖子,并更改与序列化JSON相关的周期性引用(它将在JSON的期货版本中工作) - jherax
@squint我感谢你的反馈,当人们用他的知识启发我时,我丰富了我的知识。 - jherax