Ошибка утечки памяти при закрытии JavaScript

решаемые

В Интернете много противоречивой информации по этому вопросу. Благодаря @John мне удалось понять, что закрытие (как используется ниже) не является причиной утечки памяти, и что даже в IE8 они не так распространены, как утверждают люди. Фактически в моем коде произошла только одна утечка, что оказалось не так сложно исправить.

Отныне мой ответ на этот вопрос будет:
AFAIK, единственный раз утечка IE8, - это когда привязаны события/обработчики на глобальном объекте. (window.onload window.onbeforeunload,...). Чтобы обойти это, см. Мой ответ ниже.


ОГРОМНОЕ ОБНОВЛЕНИЕ:

Теперь я полностью потерялся... Через некоторое время, пробираясь сквозь статьи и старая и новая, у меня осталось хотя бы одно огромное противоречие. Хотя один из авторов JavaScript (Douglas Crockford) говорит:

Поскольку IE не может выполнять свою работу и возвращать циклы, нам приходится это делать. Если мы явно разрываем циклы, тогда IE сможет восстановить память. По словам Microsoft, закрытие является причиной утечки памяти. Это, конечно, глубоко неправильно, но это приводит к тому, что Microsoft дает очень плохие советы программистам о том, как справиться с ошибками Microsoft. Оказывается, легко разбить циклы на стороне DOM. Их практически невозможно разбить на стороне JScript.

И как @freakish отметил, что мои фрагменты ниже похожи на внутренние работы jQuery, я чувствовал себя довольно уверенно в своем решении, не вызывая утечки памяти. В то же время я нашел эту страницу MSDN, где раздел Circular References with Closures представлял для меня особый интерес. На рисунке ниже представлено схематическое представление о том, как работает мой код, не так ли:

Circular References with Closures

Единственное различие заключается в том, что у меня есть здравый смысл не прикреплять слушателей событий к самим элементам.
Все тот же Douggie совершенно недвусмыслен: закрытие не является источником mem-утечек в IE. Это противоречие оставляет меня невежественным относительно того, кто прав.

Я также выяснил, что проблема утечки не полностью решена в IE9 (не удается найти ATM).

Последнее: я также узнал, что IE управляет DOM за пределами механизма JScript, что заставляет меня беспокоиться, когда я меняю дочерние элементы <select> на основе запроса ajax:

function changeSeason(e)
{
    var xhr,sendVal,targetID;
    e = e || window.event;//(IE...
    targetID = this.id.replace(/commonSourceFragment/,'commonTargetFragment');//fooHomeSelect -> barHomeSelect
    sendVal = this.options[this.selectedIndex].innerHTML.trim().substring(0,1);
    xhr = prepareAjax(false,(function(t)
    {
        return function()
        {
            reusableCallback.apply(this,[t]);
        }
    })(document.getElementById(targetID)),'/index/ajax');
    xhr({data:{newSelect:sendVal}});
}

function reusableCallback(elem)
{
    if (this.readyState === 4 && this.status === 200)
    {
        var data = JSON.parse(this.responseText);
        elem.innerHTML = '<option>' + data.theArray.join('</option><option>') + '</option>';
    }
}

Если IE действительно управляет DOM, как если бы механизм JScript не был там, каковы шансы, что элементы опций не будут освобождены с помощью этого кода?
Я специально добавил этот фрагмент в качестве примера, потому что в этом случае я передаю переменные, которые являются частью области закрытия, как аргумент глобальной функции. Я не мог найти документацию по этой практике, но на основании документации, предоставленной Miscrosoft, она должна разорвать любые циклические ссылки, которые могут возникнуть, не так ли?



Предупреждение: длинный вопрос... (извините)

Я написал несколько довольно больших JavaScripts, чтобы делать вызовы Ajax в моем веб-приложении. чтобы избежать множества обратных вызовов и событий, я полностью воспользовался делегированием и закрытием событий. Теперь я написал функцию, которая заставляет меня задуматься о возможных утечках памяти. Хотя я знаю, что IE > 8 сделок с закрытием намного лучше, чем его предшественники, это политика компании, чтобы поддерживать IE 8 все-таки.

Ниже я привел пример того, о чем я говорю, здесь вы можете найти аналогичный пример, хотя он не используйте ajax, но setTimeout, результат почти такой же. (Вы можете, конечно, пропустить код ниже, самому вопросу)

Код, который я имею в виду, таков:

function prepareAjax(callback,method,url)
{
    method = method || 'POST';
    callback = callback || success;//a default CB, just logs/alerts the response
    url = url || getUrl();//makes default url /currentController/ajax
    var xhr = createXHRObject();//try{}catch etc...
    xhr.open(method,url,true);
    xhr.setRequestMethod('X-Requested-with','XMLHttpRequest');
    xhr.setRequestHeader('Content-type','application/x-www-form-urlencoded');
    xhr.setRequestHeader('Accept','*/*');
    xhr.onreadystatechange = function()
    {
        callback.apply(xhr);
    }
    return function(data)
    {
        //do some checks on data before sending: data.hasOwnProperty('user') etc...
        xhr.send(data);
    }
}

Все довольно прямолинейные вещи, за исключением обратного вызова onreadystatechange. Я заметил некоторые проблемы с IE при привязке обработчика напрямую: xhr.onreadystatechange = callback;, отсюда анонимная функция. Не знаю почему, но я нашел, что это самый простой способ заставить его работать.

Как я уже сказал, я использую много делегирования событий, поэтому вы можете представить, что может оказаться полезным получить доступ к фактическому элементу/событию, которое вызвало вызов ajax. Поэтому у меня есть обработчики событий, которые выглядят так:

function handleClick(e)
{
    var target,parent,data,i;
    e = e || window.event;
    target = e.target || e.srcElement;
    if (target.tagName.toLowerCase() !== 'input' && target.className !== 'delegateMe')
    {
        return true;
    }
    parent = target;
    while(parent.tagName.toLowerCase() !== 'tr')
    {
        parent = parent.parentNode;
    }
    data = {};
    for(i=0;i<parent.cells;i++)
    {
        data[parent.cells[i].className] = parent.cells[i].innerHTML;
    }
    //data looks something like {name:'Bar',firstName:'Foo',title:'Mr.'}
    i = prepareAjax((function(t)
    {
        return function()
        {
            if (this.readyState === 4 && this.status === 200)
            {
                //check responseText and, if ok:
                t.setAttribute('disabled','disabled');
            }
        }
    })(target));
    i(data);
}

Как вы можете видеть, обратный вызов onreadystatechange - это возвращаемое значение функции, которое предоставляет ссылку на элемент target при вызове обратного вызова. Благодаря делегированию событий мне больше не нужно беспокоиться о событиях, которые могут быть связаны с этим элементом, когда я решил удалить его из DOM (что я иногда делаю).
Однако, на мой взгляд, объект вызова функции обратного вызова может оказаться слишком большим для IE JScript engine и его сборщика мусора:

Событие == > обработчик == > prepareAjax - довольно нормальная последовательность вызовов, но аргумент обратного вызова:

[Anon. func (аргумент t = target) возвращает anon. F (имеет доступ к t, который в свою очередь возвращает обратно к цели)]
  === > передается функции обратного вызова anon, вызываемой с использованием метода .apply для объекта xhr, в свою очередь, частной переменная к функции prepareAjax

Я тестировал эту "конструкцию" в FF и хром. Он прекрасно работает там, но будет ли этот вид стоп-кода закрытия после закрытия после закрытия, при каждом переходе ссылки на элемент DOM будет проблемой в IE (особенно версии до IE9)?


Нет, я не буду использовать jQuery или другие библиотеки. Мне нравится чистый JS, и я хочу знать как можно больше об этом серьезно недооцененном языке. Фрагменты кода не являются фактическими примерами копирования-вставки, но предоставляют IMO, хорошее представление о том, как я использую делегирование, закрытие и обратные вызовы во всем моем script. Поэтому, если какой-то синтаксис не совсем прав, не стесняйтесь его исправить, но это не тот вопрос, о котором идет речь, конечно.

Ответы

Ответ 1

Раньше я работал с экс-программным менеджером для JavaScript внутри Microsoft, на не-браузерном проекте EcmaScript (err.. JScr... JavaScript). У нас были длительные дискуссии о закрытии. В конце концов, дело в том, что они сложнее GC, а не невозможны. Мне пришлось бы прочитать DC-дискуссию о том, как MS "ошибается", что замыкания вызывают утечку памяти - поскольку в более старых версиях IE закрытие было, конечно, проблематичным, потому что было очень сложно собрать с реализацией MS. Мне кажется странным, что парень Yahoo попытается рассказать архитекторам MS, что известная проблема с их кодом была где-то в другом месте. Насколько я ценю его работу, я не понимаю, на каком основании он был там.

Помните, статья, ссылка на которую вы ссылаетесь выше, относится к IE6, поскольку IE7 все еще находился в тяжелом развитии на момент написания статьи.

В стороне - слава богу, IE6 мертв (не заставляйте меня раскошеливаться на похоронах). Хотя, не забывайте о своем наследии... Я еще не видел, чтобы кто-нибудь сделал убедительный аргумент, что он не был лучшим браузером в мире на первый день его выпуска - проблема в том, что они выиграли войну браузера, И вот, в чем состоит один из больших промахов их истории - они уволили команду после этого, и она сидела застойной почти 5 лет. Команда IE была до 4 или 5 парней, которые исправляли ошибки в течение многих лет, создавая огромную утечку мозгов и резко отставая от кривой. К тому времени, когда они наняли команду и поняли, где они находятся, они были годами назад из-за того, что они снова занялись монотонной кодовой базой, которую никто в действительности не понимал. Это моя перспектива как внутренняя в компании, но не связанная напрямую с этой командой.

Помните, что IE никогда не оптимизировался для закрытия, потому что не было ProtoypeJS (черт возьми, не было Rails), а jQuery едва ли мелькал в голове Resig.

На момент написания статьи они также по-прежнему ориентировались на машины с 256 мегабайтами оперативной памяти, которые также не были завершены.

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

В конце концов, я хочу сказать, что вы ссылаетесь на материал, который очень датирован. Да, избегайте замыканий в IE6, поскольку они вызывают утечку памяти - но что не было в IE6?

В конце концов, это проблема, с которой MS обратилась и продолжает обращаться. Вы собираетесь сделать некоторый уровень закрытий, так было и тогда.

Я знаю, что они делали тяжелую работу в этой области вокруг IE8 (так как мой неоспоримый проект использовал нестандартный JavaScript-движок), и эта работа продолжалась в IE9/10. StatCounter (http://gs.statcounter.com/) предлагает, чтобы IE7 снизился до 1,5% рынка, по сравнению с 6% год назад, а при разработке новых сайтов IE7 становится все менее актуальным. Вы также можете разработать для NetScape 2.0, который представил поддержку JavaScript, но это было бы чуть менее глупо.

На самом деле... Не пытайтесь переопределить ради двигателя, который больше не существует.

Ответ 2

Правильно, проведя некоторое время с помощью этого инструмента IEJSLeaksDetector, я узнал, что вещи, о которых я говорил в своем первоначальном вопросе НЕ ПРИНИМАЮТ УМЕНЬШЕНИЕ МЕМОВ. Однако появилась одна утечка, которая всплыла. К счастью, мне удалось найти решение:

У меня есть главный script, а внизу - старая школа:

window.onload = function()
{
    //do some stuff, get URI, and call:
    this['_init' + uri[0].ucFirst()](uri);//calls func like _initController
    //ucFirst is an augmentation of the String.prototype
}

Это вызывает утечку в IE8, что я не мог исправить с помощью обработчика window.onbeforeunload. Кажется, вам нужно избегать привязки обработчика к глобальному объекту. Решение заключается в закрытии и прослушивании событий, это немного фальшивка, но вот что я в итоге сделал:

(function(go)
{//use closure to avoid leaking issue in IE
    function loader()
    {
        var uri = location.href.split(location.host)[1].split('/');
        //do stuff
        if (this['_init' + uri[0].ucFirst()] instanceof Function)
        {
            this['_init' + uri[0].ucFirst()](uri);
        }
        if (!(this.removeEventListener))
        {
            this.detachEvent('onload',loader);//(fix leak?
            return null;
        }
        this.removeEventListener('load',loader,false);
    }
    if (!(go.addEventListener))
    {
        go.attachEvent('onload',loader);//(IE...
    }
    else
    {
        go.addEventListener('load',loader,false);
    }
})(window);

Таким образом, событие нагрузки (on) отключается непосредственно перед возвратом обработчика window.load, в соответствии с инструментом IEJSLeaksDetector, в моем приложении отсутствуют утечки. Я доволен этим. Я надеюсь, что этот фрагмент полезен для одного из вас - если у кого-то есть предложения по улучшению этого подхода, не стесняйтесь делать это!

Приветствия, и спасибо всем вам, кто пережил трудность чтения и попыток моего катания выше!


PS: в случае, если кто-то заботится, здесь метод ucFirst String:

if (!(String.prototype.ucFirst))
{
    String.prototype.ucFirst = function()
    {
        "use strict";
        return this.charAt(0).toUpperCase() + this.slice(1);
    };
}