Утечка памяти, связанная с запросами jQuery Ajax - PullRequest
6 голосов
/ 21 сентября 2009

У меня есть веб-страница с утечкой памяти в IE8 и Firefox; использование памяти, отображаемое в проводнике Windows, со временем продолжает расти.

На следующей странице запрашивается URL «unplanned.json», который является статическим файлом, который никогда не изменяется (хотя я устанавливаю свой HTTP-заголовок Cache-control на no-cache, чтобы убедиться, что запрос Ajax всегда выполняется). Когда он получает результаты, он очищает таблицу HTML, перебирает массив json, полученный с сервера, и динамически добавляет строку в таблицу HTML для каждой записи в массиве. Затем он ждет 2 секунды и повторяет этот процесс.

Вот вся веб-страница:

<html> <head>
    <title>Test Page</title>
    <script type="text/javascript"
     src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
</head> <body>
<script type="text/javascript">
    function kickoff() {
        $.getJSON("unplanned.json", resetTable);
    }
    function resetTable(rows) {
        $("#content tbody").empty();
        for(var i=0; i<rows.length; i++) {
            $("<tr>"
                + "<td>" + rows[i].mpe_name + "</td>"
                + "<td>" + rows[i].bin + "</td>"
                + "<td>" + rows[i].request_time + "</td>"
                + "<td>" + rows[i].filtered_delta + "</td>"
                + "<td>" + rows[i].failed_delta + "</td>"
            + "</tr>").appendTo("#content tbody");
        }
        setTimeout(kickoff, 2000);
    }
    $(kickoff);
</script>
<table id="content" border="1" style="width:100% ; text-align:center">
<thead><tr>
    <th>MPE</th> <th>Bin</th> <th>When</th> <th>Filtered</th> <th>Failed</th>
</tr></thead>
<tbody></tbody>
</table>
</body> </html>

Если это поможет, вот пример json, который я отправляю обратно (именно этот массив содержит тысячи записей вместо одной):

[
    {
        mpe_name: "DBOSS-995",
        request_time: "09/18/2009 11:51:06",
        bin: 4,
        filtered_delta: 1,
        failed_delta: 1
    }
]

РЕДАКТИРОВАТЬ: Я принял чрезвычайно полезный ответ Торана, но я чувствую, что должен опубликовать дополнительный код, так как его плагин removefromdom jQuery имеет некоторые ограничения:

  • Удаляет только отдельные элементы. Поэтому вы не можете задать ему запрос типа `$ (" # content tbody tr ")` и ожидать, что он удалит все указанные вами элементы.
  • Любой элемент, который вы удаляете вместе с ним, должен иметь атрибут `id`. Поэтому, если я хочу удалить свой `tbody`, я должен присвоить` id` моему тегу `tbody`, иначе он выдаст ошибку.
  • Он удаляет сам элемент и всех его потомков, поэтому, если вы просто хотите очистить этот элемент, вам придется заново его создать (или изменить плагин, чтобы он опустел вместо удаления).

Итак, моя страница выше модифицирована для использования плагина Торана. Для простоты я не применил ни одного общего совета по производительности , предложенного Питером . Вот страница, на которой больше нет утечек памяти:

<html>
<head>
    <title>Test Page</title>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
</head>
<body>
<script type="text/javascript">
<!--
    $.fn.removefromdom = function(s) {
        if (!this) return;

        var el = document.getElementById(this.attr("id"));

        if (!el) return;

        var bin = document.getElementById("IELeakGarbageBin");

        //before deleting el, recursively delete all of its children.
        while (el.childNodes.length > 0) {
            if (!bin) {
                bin = document.createElement("DIV");
                bin.id = "IELeakGarbageBin";
                document.body.appendChild(bin);
            }

            bin.appendChild(el.childNodes[el.childNodes.length - 1]);
            bin.innerHTML = "";
        }

        el.parentNode.removeChild(el);

        if (!bin) {
            bin = document.createElement("DIV");
            bin.id = "IELeakGarbageBin";
            document.body.appendChild(bin);
        }

        bin.appendChild(el);
        bin.innerHTML = "";
    };

    var resets = 0;
    function kickoff() {
        $.getJSON("unplanned.json", resetTable);
    }
    function resetTable(rows) {
        $("#content tbody").removefromdom();
        $("#content").append('<tbody id="id_field_required"></tbody>');
        for(var i=0; i<rows.length; i++) {
            $("#content tbody").append("<tr><td>" + rows[i].mpe_name + "</td>"
                + "<td>" + rows[i].bin + "</td>"
                + "<td>" + rows[i].request_time + "</td>"
                + "<td>" + rows[i].filtered_delta + "</td>"
                + "<td>" + rows[i].failed_delta + "</td></tr>");
        }
        resets++;
        $("#message").html("Content set this many times: " + resets);
        setTimeout(kickoff, 2000);
    }
    $(kickoff);
// -->
</script>
<div id="message" style="color:red"></div>
<table id="content" border="1" style="width:100% ; text-align:center">
<thead><tr>
    <th>MPE</th>
    <th>Bin</th>
    <th>When</th>
    <th>Filtered</th>
    <th>Failed</th>
</tr></thead>
<tbody id="id_field_required"></tbody>
</table>
</body>
</html>

ДОПОЛНИТЕЛЬНОЕ РЕДАКТИРОВАНИЕ: Я оставлю свой вопрос без изменений, хотя стоит отметить, что эта утечка памяти не имеет ничего общего с Ajax. На самом деле, следующий код будет точно таким же утечкой памяти и будет легко решаться с помощью плагина Toran removefromdom jQuery:

function resetTable() {
    $("#content tbody").empty();
    for(var i=0; i<1000; i++) {
        $("#content tbody").append("<tr><td>" + "DBOSS-095" + "</td>"
            + "<td>" + 4 + "</td>"
            + "<td>" + "09/18/2009 11:51:06" + "</td>"
            + "<td>" + 1 + "</td>"
            + "<td>" + 1 + "</td></tr>");
    }
    setTimeout(resetTable, 2000);
}
$(resetTable);

Ответы [ 3 ]

11 голосов
/ 21 сентября 2009

Я не уверен, почему Firefox не рад этому, но по опыту могу сказать, что в IE6 / 7/8 вы должны установить innerHTML = ""; на объекте, который вы хотите удалить из DOM. (если вы создали этот элемент DOM динамически, то есть)

$("#content tbody").empty(); может не выпускать эти динамически генерируемые элементы DOM.

Вместо этого попробуйте что-то вроде ниже (это плагин jQuery, который я написал, чтобы решить эту проблему).

jQuery.fn.removefromdom = function(s) {
    if (!this) return;

    var bin = $("#IELeakGarbageBin");

    if (!bin.get(0)) {
        bin = $("<div id='IELeakGarbageBin'></div>");
        $("body").append(bin);
    }

    $(this).children().each(
            function() {
                bin.append(this);
                document.getElementById("IELeakGarbageBin").innerHTML = "";
            }
    );

    this.remove();

    bin.append(this);
    document.getElementById("IELeakGarbageBin").innerHTML = "";
};

Вы бы назвали это так: $("#content").removefromdom();

Единственная проблема здесь в том, что вам нужно заново создавать таблицу каждый раз, когда вы хотите ее построить.

Кроме того, если это решит вашу проблему в IE, вы можете прочитать об этом в блоге пост , который я написал ранее в этом году, когда столкнулся с той же проблемой.

Редактировать Я обновил плагин выше, чтобы теперь он был свободен от JavaScript на 95%, поэтому он использует больше jQuery, чем предыдущая версия. Вы все еще заметите, что я должен использовать innerHTML, потому что функция jQuery html (""); не работает так же для IE6 / 7/8

4 голосов
/ 21 сентября 2009

Я не уверен насчет утечки, но ваша resetTable() функция очень неэффективна.Попробуйте сначала исправить эти проблемы и посмотрите, чем вы в конечном итоге.

  • Не добавляйте в DOM в цикле.Если вы должны выполнить DOM-манипуляции, затем добавьте фрагмент документа, а затем переместите этот фрагмент в DOM.
  • Но innerHTML в любом случае быстрее, чем DOM-манипуляции, поэтому используйте его, если можете.
  • Хранить наборы jQuery в локальных переменных - нет необходимости каждый раз перезапускать селектор.
  • Также сохранять повторяющиеся ссылки в локальной переменной.
  • При итерации поКоллекция любого рода, также храните длину в локальной переменной.

Новый код:

<html> <head>
    <title>Test Page</title>
    <script type="text/javascript"
     src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js"></script>
</head> <body>
<script type="text/javascript">
$(function()
{
    var $tbody = $("#content tbody");

    function kickoff() {
        $.getJSON("test.php", resetTable);
    }

    function resetTable(rows)
    {
        var html = ''
          , i = 0
          , l = rows.length
          , row;
        for ( ; i < l; i++ )
        {
            row = rows[i];
            html += "<tr>"
                + "<td>" + row.mpe_name + "</td>"
                + "<td>" + row.bin + "</td>"
                + "<td>" + row.request_time + "</td>"
                + "<td>" + row.filtered_delta + "</td>"
                + "<td>" + row.failed_delta + "</td>"
            + "</tr>";
        }
        $tbody.html( html );
        setTimeout(kickoff, 2000);
    }

    kickoff();
});
</script>
<table id="content" border="1" style="width:100% ; text-align:center">
<thead>
    <th>MPE</th> <th>Bin</th> <th>When</th> <th>Filtered</th> <th>Failed</th>
</thead>
<tbody></tbody>
</table>
</body> </html>

Ссылки:

0 голосов
/ 21 сентября 2009

Пожалуйста, исправьте меня, если я здесь не прав, но разве SetTimeout (fn) не предотвращает освобождение пространства памяти звонящего? Чтобы все переменные / память, выделенные во время метода resetTable (строки), оставались распределенными до завершения цикла?

Если это так, перенесение строковой конструкции и логики appendTo в другой метод может быть немного лучше, поскольку эти объекты освобождаются после каждого вызова и только в памяти возвращаемого значения (в этом случае либо строковая разметка, либо ничего, если новый метод сделал appendTo ()), останется в памяти.

По существу:

Начальный звонок на стартовый

-> Вызовы resetTable ()

-> -> SetTimeout Вызовы снова запускаются

-> -> -> Вызовы resetTable () снова

-> -> -> -> Продолжить до бесконечности

Если код действительно никогда не разрешается, дерево будет продолжать расти.

Альтернативный способ освобождения некоторой памяти, основанный на этом, был бы похож на следующий код:

function resetTable(rows) {
    appendRows(rows);
    setTimeout(kickoff, 2000);
}
function appendRows(rows)
{
    var rowMarkup = '';
    var length = rows.length
    var row;

    for (i = 0; i < length; i++)
    {
        row = rows[i];
        rowMarkup += "<tr>"
                + "<td>" + row.mpe_name + "</td>"
                + "<td>" + row.bin + "</td>"
                + "<td>" + row.request_time + "</td>"
                + "<td>" + row.filtered_delta + "</td>"
                + "<td>" + row.failed_delta + "</td>"
                + "</tr>";      
    }

    $("#content tbody").html(rowMarkup);
}

Это добавит разметку к вашему телу и затем завершит эту часть стека. Я почти уверен, что каждая итерация «строк» ​​все равно останется в памяти; однако строка разметки и тому подобное должны в конце концов освободиться.

Опять же ... я давно не смотрел SetTimeout на этом низком уровне, поэтому могу ошибаться. В любом случае, это не устранит утечку и только уменьшит скорость роста. Это зависит от того, как сборщик мусора из используемых движков JavaScript работает с циклами SetTimeout, как у вас здесь.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...