jQuery-Speicherleck mit DOM-Entfernung

Lesezeit: 8 Minuten

jQuery Speicherleck mit DOM Entfernung
Eli Gerichtsschreiber

Hier ist eine kinderleichte Webseite, die Speicher in IE8 mit jQuery verliert (ich erkenne Speicherlecks, indem ich beobachte, wie die Speichernutzung meines iexplore.exe-Prozesses im Laufe der Zeit im Windows Task-Manager wächst):

<html>
<head>
    <title>Test Page</title>
    <script type="text/javascript" src="https://stackoverflow.com/questions/1462649/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>

Anscheinend sogar beim Anrufen der jQuery.remove() Funktion habe ich immer noch einige Speicherverluste. Ich kann meine eigene Entfernungsfunktion schreiben, die wie folgt keinen Speicherverlust aufweist:

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

Das funktioniert einwandfrei und verliert keinen Speicher. Warum verliert jQuery Speicher? Ich habe eine weitere Entfernungsfunktion basierend auf erstellt jQuery.remove() und das verursacht tatsächlich ein Leck:

$.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);
    });
};

Interessanterweise scheint das Speicherleck durch jeden Aufruf verursacht zu werden, den jQuery enthält, um Speicherlecks von Ereignissen und Daten zu verhindern, die mit den gelöschten DOM-Elementen verbunden sind. Wenn ich die anrufe removeWithoutLeaking funktionieren, dann bleibt mein Gedächtnis über die Zeit konstant, aber wenn ich anrufe removeWithLeakage stattdessen wächst es einfach weiter.

Meine Frage ist, was ist mit jedem Anruf

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

könnte möglicherweise das Speicherleck verursachen?

BEARBEITEN: Tippfehler im Code behoben, die sich beim erneuten Testen als keine Auswirkung auf die Ergebnisse erwiesen haben.

WEITERE BEARBEITUNG: Ich habe einen Fehlerbericht beim jQuery-Projekt eingereicht, da dies anscheinend ein jQuery-Fehler ist: http://dev.jquery.com/ticket/5285

  • Es ist eine gute Frage, und ein großes Lob dafür, dass Sie dies gefunden haben, wenn es sich wirklich um einen Fehler handelt. Wie auch immer Sie vielleicht eine bessere Antwort von den eigentlichen jQuery-Jungs erhalten, ich würde vorschlagen, ein Ticket mit ihnen zu beginnen, wenn Sie sicher sind. dev.jquery.com

    – Puh

    22. September 2009 um 21:24 Uhr

  • Interessant. Ich kann nicht sofort einen der üblichen IE-Speicherleck-Verdächtigen erkennen, bei denen Sie eine Referenzschleife zwischen einem Host-Objekt und einem nativen Objekt erhalten. Vielleicht ist dies eine neue Art von Leck? Wissen Sie, ob es nur im IE8-Standardmodus oder im IE7-Kompatibilitätsmodus oder in Quirks oder allen dreien passiert?

    – Bobin

    22. September 2009 um 21:38 Uhr

  • @Eli – soll ich die Quelle für each() am Ende der Frage posten?

    – Russ Cam

    22. September 2009 um 21:48 Uhr

  • @bobince: Es leckt sowohl im Standard- als auch im IE7-Kompatibilitätsmodus. Ich hatte noch keine Gelegenheit, es im Quirks-Modus auszuprobieren.

    – Eli Courtwright

    22. September 2009 um 22:20 Uhr

  • @Russ: Wenn Sie der Meinung sind, dass die Ursache in jeder Methode liegen muss, fügen Sie die Quelle hinzu. Ich bin jedoch nicht überzeugt; Die Verwendung von “*” als Selektor könnte genauso gut der Schuldige sein oder die Tatsache, dass wir eine Schließung verwenden.

    – Eli Courtwright

    22. September 2009 um 22:25 Uhr

jQuery Speicherleck mit DOM Entfernung
Bobine

Ich dachte, David könnte etwas mit dem angeblichen removeChild-Leck auf der Spur sein, aber ich kann es im IE8 nicht reproduzieren … es kann durchaus in früheren Browsern passieren, aber das ist nicht das, was wir hier haben. Wenn ich Child die Divs manuell entferne, gibt es kein Leck; wenn ich jQuery zur Verwendung ändere outerHTML= '' (oder move-to-bin gefolgt von bin.innerHTML) anstelle von removeChild gibt es immer noch ein Leck.

In einem Ausschlussprozess fing ich an, Bits zu hacken remove in jQuery. Zeile 1244 in 1.3.2:

//jQuery.event.remove(this);
jQuery.removeData(this);

Das Auskommentieren dieser Zeile führte zu keinem Leck.

Schauen wir uns also event.remove an, es ruft auf data('events') um zu sehen, ob dem Element irgendwelche Ereignisse zugeordnet sind. Was ist data tun?

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

Oh. Es fügt also für jedes Element, das es versucht, eine der uuid-to-data-lookup-Eintrags-Hack-Eigenschaften von jQuery hinzu lesen data on, die jeden einzelnen Nachkommen eines Elements enthalten, das Sie entfernen! Wie albern. Ich kann das kurzschließen, indem ich diese Zeile direkt davor setze:

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

was das Leck für diesen Fall in IE8 zu beheben scheint. Ich kann nicht garantieren, dass es nicht etwas anderes im Labyrinth, das jQuery ist, kaputt macht, aber logischerweise macht es Sinn.

Soweit ich das beurteilen kann, ist das Leck einfach das jQuery.cache Das Objekt (das ist der Datenspeicher, nicht wirklich ein Cache als solches) wird immer größer, da für jedes entfernte Element ein neuer Schlüssel hinzugefügt wird. Obwohl removeData diese Cache-Einträge entfernen sollte, scheint IE den Speicherplatz nicht wiederherzustellen, wenn Sie delete ein Schlüssel von einem Objekt.

(Wie auch immer, dies ist ein Beispiel für die Art von jQuery-Verhalten, das ich nicht schätze. Es tut viel zu viel unter der Haube für eine eigentlich trivial einfache Operation … von denen einige ziemlich fragwürdig sind. Das Ganze Sache mit dem Expando und was jQuery damit macht innerHTML über Regex, um zu verhindern, dass die Anzeige als Attribut im IE nur kaputt und hässlich ist. Und die Angewohnheit, Getter und Setter dieselbe Funktion zu geben, ist verwirrend und führt hier zu dem Fehler.)

[Weirdly, leaving the leaktest for extended periods of time ended up occasionally giving totally spurious errors in jquery.js before the memory actually ran out… there was something like ‘unexpected command’, and I noted a ‘nodeName is null or not an object’ at line 667, which as far as I can see shouldn’t even have been run, let alone that there is a check there for nodeName being null! IE is not giving me much confidence here…]

  • Ja, das behebt es definitiv. Am Ende werde ich wahrscheinlich eine weitere Frage stellen, in der ich frage, warum, da die jQuery.removeData-Funktion, wie Sie darauf hinweisen, diese Einträge entfernen sollte. Oh, vielen Dank für die großartige Hilfe.

    – Eli Courtwright

    23. September 2009 um 13:19 Uhr

  • Es entfernt die Einträge, aber das Cache-Objekt kann seine Speichernutzung selbst mit weniger Eigenschaften nicht verkleinern. Versuchen Sie vielleicht, IDs wiederzuverwenden, indem Sie einen Zähler für die niedrigste freie ID beibehalten, anstatt jedes Mal eine neue UUID zu erstellen? In beiden Fällen sollte das Lesen von Daten aus einem Objekt ohne Daten keinen leeren Datencontainer hinzufügen.

    – Bobin

    23. September 2009 um 13:28 Uhr

  • +1 und herzlichen Glückwunsch, dass Sie inot jQuery maze lesen können.

    – Marco Demaio

    3. September 10 um 13:14 Uhr

  • @bobince wissen Sie, ob dies in späteren Versionen von jQuery behoben wurde? Ich habe das gleiche Problem wie beschrieben, aber jetzt sieht der Code anders aus, sodass ich Ihren Fix nicht so verwenden kann, wie er ist. Ich muss das Labyrinth noch einmal neu berechnen …

    – Mohoch

    26. Dezember 2012 um 08:48 Uhr

Scheint in jQuery 1.5 (Version vom 23. Februar) behoben zu sein. Ich bin mit 1.4.2 auf das gleiche Problem gestoßen und habe es zuerst mit Dom-Entfernung wie oben beschrieben und dann durch Ausprobieren der neuen Version behoben.

Das Entfernen von Elementen ist ein inhärentes DOM-Problem. Was bei uns bleiben wird. Dito.

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

Anstatt jQ zu hacken, verwende ich diese Erweiterung. Besonders auf den Seiten mit vielen Removes() und Clones() :

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

Und auch das nächste hilft:

// remove all event bindings , 
// and the jQ data made for jQ event handling
jQuery.unbindall = function () { jQuery('*').unbind(); }
//
$(document).unload(function() { 
  jQuery.unbindall();
});

Siehe die jQuery 1.4-Roadmap unter http://docs.jquery.com/JQuery_1.4_Roadmap. Insbesondere befasst sich der Abschnitt „Use .outerHTML to cleanup after .remove()“ mit Speicherverlustproblemen, die im IE aufgrund des Aufrufs der Entfernungsfunktion auftreten.

Vielleicht werden Ihre Probleme mit der nächsten Version behoben.

Leckt es immer noch, wenn Sie anrufen empty anstatt remove?

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

  • Ja, das habe ich versucht. Ich habe sogar in jQuery unter die Haube geschaut und festgestellt, dass empty selbst remove aufruft, um den größten Teil seiner Arbeit zu erledigen.

    – Eli Courtwright

    22. September 2009 um 21:26 Uhr

JQuery 1.4.1 hat Folgendes:

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

Folgendes musste ich ändern, um das Leckageproblem zu beseitigen:

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

zusätzliche Funktion:

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

  • Ja, das habe ich versucht. Ich habe sogar in jQuery unter die Haube geschaut und festgestellt, dass empty selbst remove aufruft, um den größten Teil seiner Arbeit zu erledigen.

    – Eli Courtwright

    22. September 2009 um 21:26 Uhr

.

752490cookie-checkjQuery-Speicherleck mit DOM-Entfernung

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy