Риск утечки памяти в закрытиях JavaScript

решаемая

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

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


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

Я полностью потерян сейчас... Через некоторое время, копаясь в статьях и рассказывая как старое, так и новое, у меня остается по крайней мере одно огромное противоречие. В то время как один из Гуру JavaScript (Дуглас Крокфорд) говорит:

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

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

Циркулярные ссылки с замыканиями

Единственная разница в том, что у меня есть здравый смысл не привязывать слушателей событий к самим элементам.
Все тот же Дуги совершенно недвусмысленен: замыкания не являются источником утечек памяти в 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, каковы шансы, что элементы option не были освобождены при использовании этого кода?
Я намеренно добавил этот фрагмент в качестве примера, потому что в этом случае я передаю переменные, которые являются частью области замыкания, в качестве аргумента глобальной функции. Я не смог найти никакой документации по этой практике, но, основываясь на документации, предоставленной 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 (что я иногда делаю).
Однако, на мой взгляд, объект вызова функции обратного вызова может оказаться слишком большим для JScript-движка IE и его сборщика мусора:

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

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

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


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

2 ответа

Решение

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

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

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

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

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

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

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

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

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

Действительно... Не пытайтесь чрезмерно оптимизировать ради движка, которого больше нет.

Хорошо, проведя некоторое время с этим IEJSLeaksDetector Я обнаружил, что вещи, о которых я говорил в моем первоначальном вопросе, не вызывают уток. Однако, была 1 утечка, которая действительно выскакивала. К счастью, мне удалось найти решение:

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

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)load не связано непосредственно перед window.load обработчик возвращает, согласно инструменту IEJSLeaksDetector, в моем приложении нет утечек. Я доволен этим. Я надеюсь, что этот фрагмент окажется полезным для одного из вас - если у кого-то есть предложения по улучшению этого подхода, не стесняйтесь делать это!

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


PS: в случае, если кому-то все равно, вот метод ucFirst String:

if (!(String.prototype.ucFirst))
{
    String.prototype.ucFirst = function()
    {
        "use strict";
        return this.charAt(0).toUpperCase() + this.slice(1);
    };
}
Другие вопросы по тегам