Утечка памяти с использованием запросов JQuery Ajax

У меня есть веб-страница, которая пропускает память как в 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
    }
]

EDIT: я принял Торан чрезвычайно полезный ответ, но я чувствую, что должен опубликовать дополнительный код, так как его плагин removefromdom jQuery имеет некоторые ограничения:

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

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

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

Ответ 1

Я не уверен, почему 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, вы можете прочитать об этом более подробно в блоге post, который я написал ранее в этом году, когда я столкнулись с одной и той же проблемой.

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

Ответ 2

Я не уверен в утечке, но ваша функция 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>

Литература:

Ответ 3

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

Если этот случай, толкание строковой конструкции и 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, как вы здесь.