【问题标题】:How to avoid memory leaks from jQuery?如何避免 jQuery 的内存泄漏?
【发布时间】:2015-08-27 20:57:24
【问题描述】:

jQuery 在其内部缓存中保存对 DOM 节点的引用,直到我显式调用 $.remove()。如果我使用 React 等框架自行移除 DOM 节点(使用原生 DOM 元素 API),我该如何清理 jQuery 的内存缓存?

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

Flash forward,我们决定使用 jQuery 插件。在 React 运行它的渲染循环并构建 DOM 之后,我们初始化插件,使 jQuery 持有对相应 DOM 节点的引用。稍后,用户更改页面上的选项卡,React 删除这些 DOM 元素。不幸的是,由于 React 不使用 jQuery 的 $.remove() 方法,jQuery 维护对那些 DOM 元素的引用,并且垃圾收集器永远不会清除它们。

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

【问题讨论】:

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

标签: javascript jquery memory-leaks reactjs


【解决方案1】:

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

内部方法具有以下签名:

jQuery._data( DOMElement, data)

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

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

这会返回 Object,其中包含 事件类型 作为键,事件处理程序数组 作为值。

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

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

这将返回 “click”事件处理程序的 Array 或 undefined(如果指定的事件未绑定到元素)。

为什么我要讲这个方法?因为它允许我们追踪事件委托和直接附加的事件监听器,这样我们就可以找出一个事件处理程序多次绑定到同一个元素,导致内存泄漏。

但是如果你也想在没有 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 元素的事件

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

我鼓励任何阅读这篇文章的人注意我们代码中的内存分配,我了解到由于三个重要原因而出现的性能问题:

  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);

循环引用

虽然 circular references 对于那些在 Garbage Collector 中实现 Mark-and-sweep algorithm 的浏览器不再是问题,但使用这种如果我们正在交换数据,则对象,因为(目前)不可能序列化为 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:这篇文章是关于Cloning Objects in JavaScript。此要点还包含有关使用循环引用克隆对象的演示:clone.js


重用对象

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

//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);

还有很多方法可以改进我们的代码,对性能产生影响,并防止内存泄漏。在这篇文章中,我主要讨论了事件,但还有其他可能导致内存泄漏的方式。我建议阅读之前发布的文章。


阅读愉快,编码愉快!

【讨论】:

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

如果您的插件公开了一种以编程方式销毁其实例之一的方法(即$(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" />;
    },
});

如果您的插件没有公开一种自行清理的方法,this article 列出了一些您可以尝试取消引用考虑不周的插件的方法。

但是,如果您在 React 组件中使用 jQuery创建 DOM 元素,那么您做错了:在使用 jQuery 时,您几乎应该从不需要 jQuery React,因为它已经抽象出使用 DOM 的所有痛点。

我也会对使用 refs 保持警惕。真正需要 ref 的用例很少,通常涉及与操作/读取 DOM 的第三方库的集成。


如果您的组件有条件地渲染受 jQuery 插件影响的元素,您可以使用回调 refs 来监听其挂载/卸载事件。

之前的代码会变成:

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>
        );
    },
});

【讨论】:

  • 我没有使用过 React,但现在我使用 Angular 进行,您提出了一个很好的观点:在使用 X 时几乎不需要 jQuery ... 当 X 是一个与 DOM 分离的库时。
  • 使用 jQuery 的 .remove() 销毁插件甚至 DOM 元素都可以在 componentWillUnmount 中完成,但是在组件的生命周期中仍然有很多机会让 render 方法拆除并重建 DOM。 componentWillUnmount 只覆盖最后一个。
  • @BrentTraut 好点。我已经更新了我的答案以涵盖这个案例。
猜你喜欢
  • 1970-01-01
  • 2020-05-13
  • 1970-01-01
  • 1970-01-01
  • 2018-04-08
  • 2013-06-24
  • 1970-01-01
相关资源
最近更新 更多