jQuery内存泄漏与DOM删除

46

这是一个非常简单的网页,使用jQuery在IE8中会泄漏内存(我通过观察Windows任务管理器中iexplore.exe进程的内存使用情况来检测内存泄漏):

<html>
<head>
    <title>Test Page</title>
    <script type="text/javascript" src="jquery.js"></script>
</head>
<body>
<script type="text/javascript">
    function resetContent() {
        $("#content div").remove();
        for(var i=0; i<10000; i++) {
            $("#content").append("<div>Hello World!</div>");
        }
        setTimeout(resetTable, 2000);
    }
    $(resetContent);
</script>
<div id="content"></div>
</body>
</html>

显然即使调用jQuery.remove()函数,我仍然会遇到一些内存泄漏问题。 我可以编写自己的 remove 函数,如下所示:

$.fn.removeWithoutLeaking = function() {
    this.each(function(i,e){
        if( e.parentNode )
            e.parentNode.removeChild(e);
    });
};

这段代码运行良好且不会泄漏内存。那么为什么jQuery会泄漏内存呢?我根据jQuery.remove()创建了另一个remove函数,但这确实会导致泄漏:

$.fn.removeWithLeakage = function() {
    this.each(function(i,e) {
        $("*", e).add([e]).each(function(){
            $.event.remove(this);
            $.removeData(this);
        });
        if (e.parentNode)
            e.parentNode.removeChild(e);
    });
};

有趣的是,内存泄漏似乎是由jQuery调用的每个函数引起的。这些函数旨在防止与DOM元素相关的事件和数据被删除时发生内存泄漏。当我调用removeWithoutLeaking函数时,我的内存随时间保持恒定,但当我改为调用removeWithLeakage函数时,内存却不断增长。

我的问题是,那个每个函数具体是什么?

$("*", e).add([e]).each(function(){
    $.event.remove(this);
    $.removeData(this);
});

可能会导致内存泄漏的原因是什么?

编辑:修复了代码中的拼写错误,重新测试后发现对结果没有影响。

进一步编辑:我向jQuery项目提交了一个错误报告,因为这似乎是一个jQuery的bug:http://dev.jquery.com/ticket/5285


1
这是一个好问题,如果这真的是一个错误,那么感谢您发现了它。然而,您可能会从jQuery团队获得更好的回应,我建议您如果确定的话可以向他们提交一个工单。http://dev.jquery.com/ - womp
1
有趣。我立即看不到任何通常的IE内存泄漏嫌疑人,在主机对象和本地对象之间形成引用循环,也许这是一种新的泄漏?您知道它只发生在IE8标准模式中,还是在IE7兼容模式、Quirks模式或所有三种模式中都发生? - bobince
@Eli - 我应该将 each() 的源代码发布到问题的末尾吗? - Russ Cam
@bobince: 在标准模式和IE7兼容模式下都有泄漏问题。我还没有测试它在怪异模式下的情况。 - Eli Courtwright
@Russ:如果你认为问题出在每个方法中,那就加上源代码吧。但我并不完全相信;使用“*”作为选择器同样可能是罪魁祸首,或者我们使用了闭包。 - Eli Courtwright
最好编辑原始问题以指示使用的jQuery版本。我还在研究与jQuery相关的IE内存泄漏问题,但很难知道问题是否已经修复。 - Jiho Han
6个回答

59
我认为David在所谓的removeChild泄漏方面可能有所发现,但我无法在IE8中复现它……它可能会在早期的浏览器中发生,但这不是我们现在拥有的。如果我手动删除div,则没有泄漏;如果我改变jQuery使用outerHTML= ''(或移动到bin后跟随bin.innerHTML)而不是removeChild,则仍会泄漏。
为了排除干扰,我开始破解jQuery中的remove位。1.3.2中的第1244行:
//jQuery.event.remove(this);
jQuery.removeData(this);

将那行代码注释掉后就没有内存泄漏了。

现在,让我们来看一下event.remove,它调用data('events')来查看元素是否有任何事件附加。那么data在做什么?

// Compute a unique ID for the element
if ( !id )
    id = elem[ expando ] = ++uuid;

哦,所以它为每个尝试读取数据的元素添加了jQuery的uuid-to-data-lookup条目hack属性,这包括您要删除的元素的每个后代!多么愚蠢。我可以通过在它之前放置这一行来短路它:

// Don't create ID/lookup if we're only reading non-present data
if (!id && data===undefined)
    return undefined;

看起来这个修复方法可以解决IE8中的泄漏问题。不能保证它不会破坏jQuery迷宫中的其他东西,但从逻辑上讲是有意义的。

据我所知,泄漏只是jQuery.cache对象(数据存储,而不是真正的缓存)随着每个已删除元素的添加而变得越来越大。虽然removeData应该可以正确地删除那些缓存条目,但是当你从一个对象中delete一个键时,IE似乎没有恢复空间。

(无论如何,这是我不欣赏的jQuery行为的例子之一。对于应该是微不足道的简单操作,它在幕后做了太多事情了...其中一些是相当可疑的。整个expando的事情以及jQuery通过正则表达式对innerHTML进行处理以防止在IE中将其显示为属性就很糟糕。使getter和setter成为同一个函数的习惯很令人困惑,在这里导致了错误。)

【奇怪的是,将泄漏测试留给较长时间后,实际内存用尽之前jquery.js偶尔会出现完全虚假的错误...有类似“意外命令”的东西,我注意到在第667行有一个“nodeName为null或不是对象”,在我看来甚至不应该运行,更不用说那里还有一个检查nodeName是否为null的检查!IE在这里给我带来了很少信心...】


是的,这绝对解决了问题。我可能最终会发另一个问题询问为什么,因为正如您指出的那样,jQuery.removeData函数应该清除这些条目。不管怎样,非常感谢您的帮助。 - Eli Courtwright
1
它会清除条目,但缓存对象即使少了属性,仍无法减少其内存使用量。也许尝试通过保持“最低空闲ID”计数器来重新使用ID,而不是每次都创建新的UUID?无论哪种方式,从没有数据的对象中读取数据不应该添加一个空数据容器。 - bobince
1
恭喜您能够读懂jQuery迷宫,加油! - Marco Demaio
4
你好,@bobince,你知道这个问题在 jQuery 的后续版本中是否得到解决了吗?我遇到了与描述相同的问题,但现在的代码看起来不同,所以我不能直接使用你的修复方法。我需要重新摸索解决方案... - Mohoch

5

看起来在jQuery 1.5版(2月23日版本)中已经修复了此问题。我曾经遇到过1.4.2版本的同样问题,首先通过上述方法进行DOM移除修复,然后尝试使用新版本解决问题。


4

元素删除是DOM中固有的问题,这个问题将一直存在。同样道理。

jQuery.fn.flush = function()
/// <summary>
/// $().flush() re-makes the current element stack inside $() 
/// thus flushing-out the non-referenced elements
/// left inside after numerous remove's, append's etc ...
/// </summary>
{ return jQuery(this.context).find(this.selector); }

我不会直接修改jQ,而是使用这个扩展。特别是在有很多removes()和clones()的页面上:

$exact = $("whatever").append("complex html").remove().flush().clone();

并且下一个也有帮助:
// remove all event bindings , 
// and the jQ data made for jQ event handling
jQuery.unbindall = function () { jQuery('*').unbind(); }
//
$(document).unload(function() { 
  jQuery.unbindall();
});

2
请查看jQuery 1.4路线图,网址为http://docs.jquery.com/JQuery_1.4_Roadmap。具体来说,"使用.outerHTML清理.remove()后的内容"这一部分解决了IE中由于调用remove函数而导致的内存泄漏问题。也许您的问题将在下一个版本中得到解决。

1
JQuery 1.4.1 包含以下内容:
    cleanData: function (elems) {
        for (var i = 0, elem, id; (elem = elems[i]) != null; i++) {
            jQuery.event.remove(elem);
            jQuery.removeData(elem);
        }
    }

这是我为了消除泄漏问题所做的修改:

    cleanData: function (elems) {
        for (var i = 0, elem, id; (elem = elems[i]) != null; i++) {
            jQuery.event.remove(elem);
            jQuery.removeData(elem);
            jQuery.purge(elem);
        }
    }

新增功能:

    purge: function (d) {
        var a = d.childNodes;
        if (a) {
            var remove = false;
            while (!remove) {
                var l = a.length;
                for (i = 0; i < l; i += 1) {
                    var child = a[i];
                    if (child.childNodes.length == 0) {
                        jQuery.event.remove(child);
                        d.removeChild(child);
                        remove = true;
                        break;
                    }
                    else {
                        jQuery.purge(child);
                    }
                }
                if (remove) {
                    remove = false;
                } else {
                    break;
                }
            }
        }
    },

我对清除操作进行了修改,以使其更加高效(至少提高30%)。 - Basil
1
这不仅不起作用,在大量元素集上还会使IE变得缓慢。它在内存使用、线程计数和CPU使用率不断攀升的情况下停滞了10分钟。对于少量元素来说,它可以正常工作,但对于使用大量内存的大量元素来说就不行了...这也是问题所在。 - AS7K

1

如果你调用empty而不是remove,它是否仍会泄漏?

$("#content").empty();

1
是的,我尝试过了。我甚至在 jQuery 下看了看内部实现,发现 empty 本身调用 remove 来完成大多数工作。 - Eli Courtwright

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接