Как избежать утечек памяти из jQuery?

jQuery хранит ссылки на узлы DOM во внутреннем кеше, пока я явно не вызову $.remove(). Если я использую структуру, такую ​​как React, которая сама удаляет узлы DOM (используя собственные API-интерфейсы DOM), как очистить кеш-память jQuery?

Я разрабатываю довольно большое приложение, использующее React. Для тех, кто незнакомец, React срывает DOM и восстанавливает по мере необходимости на основе собственного "теневого" представления DOM. Эта деталь отлично работает без утечек памяти.

Вперед, мы решили использовать плагин jQuery. После того, как React выполнит цикл рендеринга и построит DOM, мы инициализируем плагин, который заставляет jQuery удерживать ссылку на соответствующие узлы DOM. Позже пользователь меняет вкладки на странице, а React удаляет эти элементы DOM. К сожалению, поскольку React не использует метод jQuery $.remove(), jQuery поддерживает ссылку на эти элементы DOM, и сборщик мусора никогда не очищает их.

Есть ли способ, которым я могу сказать jQuery, чтобы очистить его кеш, или еще лучше, чтобы не кэшировать вообще? Мне бы очень хотелось, чтобы я мог использовать jQuery для своих плагинов и кроссбраузерной доброты.

Ответ 1

Если ваш плагин предоставляет метод для программного уничтожения одного из его экземпляров (т.е. $(element).plugin('destroy')), вы должны вызывать это в жизненном цикле componentWillUnmount вашего компонента.

componentWillUnmount вызывается прямо перед тем, как ваш компонент будет удален из DOM, это место для очистки всех внешних ссылок/прослушивателей событий /dom, которые ваш компонент мог создать за время своей жизни.

var MyComponent = React.createClass({
    componentDidMount() {
        $(React.findDOMNode(this.refs.jqueryPluginContainer)).plugin();
    },
    componentWillUnmount() {
        $(React.findDOMNode(this.refs.jqueryPluginContainer)).plugin('destroy');
    },
    render() {
        return <div ref="jqueryPluginContainer" />;
    },
});

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

Однако, если вы создаете элементы DOM с помощью jQuery из вашего компонента React, то вы делаете что-то серьезно неправильное: вы должны почти никогда нуждаться в jQuery при работе с Реагируйте, так как он уже абстрагирует все точки боли в работе с DOM.

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


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

Предыдущий код:

var MyComponent = React.createClass({
    handlePluginContainerLifecycle(component) {
        if (component) {
            // plugin container mounted
            this.pluginContainerNode = React.findDOMNode(component);
            $(this.pluginContainerNode).plugin();
        } else {
            // plugin container unmounted
            $(this.pluginContainerNode).plugin('destroy');
        }
    },
    render() {
        return (
            <div>
                {Math.random() > 0.5 &&
                    // conditionally render the element
                    <div ref={this.handlePluginContainerLifecycle} />
                }
            </div>
        );
    },
});

Ответ 2

jQuery отслеживает события и другие типы данных через внутренний API jQuery._data(), однако из-за этого метода является внутренним, он не имеет официальной поддержки.

Внутренний метод имеет следующую подпись:

jQuery._data( DOMElement, data)

Таким образом, например, мы собираем все обработчики событий, прикрепленные к элементу (через jQuery):

var allEvents = jQuery._data( document, 'events');

Это возвращает и Object, содержащий ключ тип события, и массив обработчиков событий.

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

var clickHandlers = (jQuery._data(document, 'events') || {}).click;

Это возвращает Array обработчиков событий "click" или undefined, если указанное событие не привязано к элементу.

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

Но если вам нужна аналогичная функциональность без jQuery, вы можете достичь ее с помощью метода getEventHandlers

Взгляните на эти полезные статьи:


Отладка

Мы собираемся написать простую функцию, которая печатает обработчики событий и их пространство имен (если оно было указано)

function writeEventHandlers (dom, event) {
    jQuery._data(dom, 'events')[event].forEach(function (item) {
        console.info(new Array(40).join("-"));
        console.log("%cnamespace: " + item.namespace, "color:orangered");
        console.log(item.handler.toString());
    });
}

Использование этой функции довольно просто:

writeEventHandlers(window, "resize");

Я написал несколько утилит, которые позволяют нам отслеживать события, связанные с элементами DOM

И если вы заботитесь о производительности, вы найдете полезные ссылки:

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

  • Память
  • Память
  • И да, память.

События: хорошие практики

Это хорошая идея создать именованные функции для привязать и отменить обработчики событий из элементов DOM.

Если вы динамически создаете элементы DOM и, например, добавляете обработчики к некоторым событиям, вы можете использовать делегирование событий вместо того, чтобы держать ограничивающих прослушивателей событий непосредственно каждому элементу, таким образом, родительский динамически добавленных элементов обработает событие. Также, если вы используете jQuery, вы можете пропустить пробелы в событиях;)

//the worse!
$(".my-elements").click(function(){});

//not good, anonymous function can not be unbinded
$(".my-element").on("click", function(){});

//better, named function can be unbinded
$(".my-element").on("click", onClickHandler);
$(".my-element").off("click", onClickHandler);

//delegate! it is bound just one time to a parent element
$("#wrapper").on("click.nsFeature", ".my-elements", onClickMyElement);

//ensure the event handler is not bound several times
$("#wrapper")
    .off(".nsFeature1 .nsFeature2") //unbind event handlers by namespace
    .on("click.nsFeature1", ".show-popup", onShowPopup)
    .on("click.nsFeature2", ".show-tooltip", onShowTooltip);

Циркулярные ссылки

Хотя круговые ссылки больше не проблема для тех браузеров, которые реализуют Mark-and-sweep algorithm в своем Garbage Collector, это не разумная практика использования таких объектов, если мы меняем данные, потому что это не (на данный момент) сериализуется в JSON, но в будущих выпусках это будет возможно благодаря новому алгоритму, который обрабатывает такие объекты. Рассмотрим пример:

var o1 = {};
    o2 = {};
o1.a = o2; // o1 references o2
o2.a = o1; // o2 references o1

//now we try to serialize to JSON
var json = JSON.stringify(o1);
//we get:"Uncaught TypeError: Converting circular structure to JSON"

Теперь попробуйте выполнить этот другой пример

var freeman = {
    name: "Gordon Freeman",
    friends: ["Barney Calhoun"]
};

var david = {
    name: "David Rivera",
    friends: ["John Carmack"]
};

//we create a circular reference
freeman.friends.push(david); //freeman references david
david.friends.push(freeman); //david references freeman

//now we try to serialize to JSON
var json = JSON.stringify(freeman);
//we get:"Uncaught TypeError: Converting circular structure to JSON"

PD: Эта статья посвящена Клонирование объектов в JavaScript. Также этот gist содержит демонстрации о клонировании объектов с круговыми ссылками: clone.js


Повторное использование объектов

Давайте рассмотрим некоторые принципы программирования, DRY (не повторяйте сами), и вместо создания новых объектов с аналогичной функциональностью мы можем абстрагировать их по-настоящему. В этом примере я собираюсь повторно использовать обработчик событий (снова с событиями)

//the usual way
function onShowContainer(e) {
    $("#container").show();
}
function onHideContainer(e) {
    $("#container").hide();
}
$("#btn1").on("click.btn1", onShowContainer);
$("#btn2").on("click.btn2", onHideContainer);
 
//the good way, passing data to events
function onToggleContainer(e) {
    $("#container").toggle(e.data.show);
}
$("#btn1").on("click.btn1", { show: true }, onToggleContainer);
$("#btn2").on("click.btn2", { show: false }, onToggleContainer);

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


Счастливое чтение и счастливое кодирование!

Ответ 3

Как это сделать, когда пользователь выходит из закладки:

for (x in window) {
    delete x;
}

Это гораздо лучше сделать, хотя:

for (i in $) {
    delete i;
}