46 votes

Fuite mémoire jQuery avec suppression du DOM

Voici une page web très simple qui fait fuir la mémoire dans IE8 en utilisant jQuery (je détecte les fuites de mémoire en regardant l'utilisation de la mémoire de mon processus iexplore.exe augmenter au fil du temps dans le gestionnaire des tâches de Windows) :

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

Apparemment, même en appelant le jQuery.remove() Je constate toujours des fuites de mémoire. Je peux écrire ma propre fonction de suppression qui ne subit aucune fuite de mémoire comme suit :

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

Cela fonctionne parfaitement et n'entraîne aucune perte de mémoire. Alors pourquoi jQuery fait-il fuir de la mémoire ? J'ai créé une autre fonction de suppression basée sur jQuery.remove() et cela provoque effectivement une fuite :

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

Il est intéressant de noter que la fuite de mémoire semble être causée par l'appel de each que jQuery inclut pour éviter les fuites de mémoire dues aux événements et aux données associés aux éléments du DOM qui sont supprimés. Lorsque j'appelle la fonction removeWithoutLeaking alors ma mémoire reste constante dans le temps, mais lorsque j'appelle la fonction removeWithLeakage au lieu de ça, ça continue de croître.

Ma question est, qu'en est-il de chaque appel

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

pourrait être à l'origine de la fuite de mémoire ?

EDIT : Correction d'une erreur de frappe dans le code qui, après un nouveau test, s'est avérée n'avoir aucun effet sur les résultats.

NOUVELLE ÉDITION : J'ai rempli un rapport de bogue avec le projet jQuery, puisque cela semble être un bogue de jQuery : http://dev.jquery.com/ticket/5285

58voto

bobince Points 270740

J'ai pensé que David était peut-être sur une piste avec la prétendue fuite removeChild, mais je n'arrive pas à la reproduire dans IE8... elle peut très bien se produire dans des navigateurs antérieurs, mais ce n'est pas ce que nous avons ici. Si je retire manuellement les divs, il n'y a pas de fuite ; si je modifie jQuery pour utiliser outerHTML= '' (ou move-to-bin suivi de bin.innerHTML) au lieu de removeChild, il y a toujours une fuite.

Dans un processus d'élimination, j'ai commencé à pirater des morceaux de remove dans jQuery. Ligne 1244 dans la version 1.3.2 :

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

L'élimination de cette ligne n'a donné lieu à aucune fuite.

Donc, regardons event.remove, il appelle data('events') pour voir s'il y a des événements liés à l'élément. Ce qui est data à faire ?

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

Oh. Donc, il ajoute une des propriétés de jQuery uuid-to-data-lookup pour chaque élément qu'il essaie même de lire qui inclut chaque descendant d'un élément que vous supprimez ! C'est idiot. Je peux court-circuiter cela en mettant cette ligne juste avant :

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

qui semble corriger la fuite pour ce cas dans IE8. Je ne peux pas garantir que cela ne cassera pas quelque chose d'autre dans le labyrinthe qu'est jQuery, mais logiquement, c'est logique.

D'après ce que j'ai compris, la fuite est simplement la jQuery.cache L'objet (qui est le magasin de données, pas vraiment un cache en tant que tel) devient de plus en plus grand à mesure qu'une nouvelle clé est ajoutée pour chaque élément supprimé. Bien que removeData devrait supprimer ces entrées de cache sans problème, IE ne semble pas récupérer l'espace lorsque vous delete une clé d'un objet.

(Quoi qu'il en soit, c'est un exemple du type de comportement de jQuery que je n'apprécie pas. Il fait beaucoup trop de choses sous le capot pour ce qui devrait être une opération trivialement simple... dont certaines sont assez douteuses. Tout ce qui concerne l'expansion et ce que jQuery fait pour innerHTML par le biais d'une expression rationnelle afin d'éviter qu'elle n'apparaisse comme un attribut dans IE. est juste cassé et moche. Et l'habitude de faire du getter et du setter la même fonction est source de confusion et, ici, aboutit au bug).

[Bizarrement, laisser le test de fuite pendant de longues périodes de temps a fini par donner occasionnellement des erreurs totalement fausses dans jquery.js avant que la mémoire ne s'épuise réellement... il y avait quelque chose comme 'unexpected command', et j'ai noté un 'nodeName is null or not an object' à la ligne 667, qui, d'après ce que je peux voir, n'aurait même pas dû être exécuté, sans parler du fait qu'il y a une vérification pour nodeName étant nul ! IE ne me donne pas beaucoup de confiance ici...].

5voto

Ben Points 66

Il semble être corrigé dans jQuery 1.5 (version du 23 février). J'ai rencontré le même problème avec la version 1.4.2 et je l'ai résolu d'abord en supprimant le dom comme ci-dessus, puis en essayant la nouvelle version.

4voto

DBJDBJ Points 478

La suppression des éléments est un problème inhérent à DOM. Qui va rester avec nous. Idem.

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

Au lieu de pirater jQ, j'utilise cette extension. Surtout dans les pages avec beaucoup de removes() et clones() :

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

Et aussi la prochaine aide :

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

2voto

David Andres Points 13569

Consultez la feuille de route de jQuery 1.4 à l'adresse suivante http://docs.jquery.com/JQuery_1.4_Roadmap . Plus précisément, la section "Use .outerHTML to cleanup after .remove()" traite des problèmes de fuite de mémoire survenant dans IE en raison de l'appel de la fonction remove.

Peut-être vos problèmes seront-ils résolus avec la prochaine version.

1voto

Basil Points 19

JQuery 1.4.1 a les caractéristiques suivantes :

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

Voici ce que j'ai dû modifier pour éliminer le problème de fuite :

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

fonction ajoutée :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X