Вход на курс повышения квалификации по интерфейсному интерфейсу (vue): поступление на изучение JavaScript не предусматривает никаких операций по управлению памятью. Вместо этого памятью управляет виртуальная машина JavaScript посредством процесса освобождения памяти, называемого сборкой мусора .
Поскольку мы не можем принудительно выполнить сборку мусора, как мы узнаем, что она работает? Как много мы об этом знаем?
Выполнение скрипта приостанавливается во время этого процесса
Он освобождает память для недоступных ресурсов.
это неопределенно
Он не проверяет всю память сразу, а выполняется в несколько циклов
Он непредсказуем, но сработает, когда это необходимо.
Означает ли это, что не нужно беспокоиться о проблемах с распределением ресурсов и памяти. Конечно, нет? Если мы не будем осторожны, могут возникнуть некоторые утечки памяти.
Утечка памяти — это блок выделенной памяти, который программное обеспечение не может вернуть.
Javascript предоставляет сборщик мусора, но это не значит, что мы можем избежать утечек памяти. Чтобы иметь право на сбор мусора, на объект не должно ссылаться где-либо еще. Если вы храните ссылки на неиспользуемые ресурсы, это предотвратит их возвращение. Это называется бессознательным сохранением памяти .
Утечка памяти может привести к более частому запуску сборщика мусора. Так как этот процесс будет препятствовать запуску скрипта, это может привести к зависанию нашей программы. Если произойдет такая задержка, привередливые пользователи обязательно заметят, что, если их это не устраивает, продукт будет находиться в автономном режиме в течение длительного времени. Более серьезно, это может привести к сбою всего приложения, а именно gg.
Как предотвратить утечки памяти? Главное — избегать сохранения ненужных ресурсов. Давайте рассмотрим некоторые распространенные сценарии.
Метод setInterval()
неоднократно вызывает функцию или выполняет фрагмент кода с фиксированной задержкой между каждым вызовом. Он возвращает ID
интервала ID
однозначно идентифицирует интервал, чтобы вы могли позже удалить его, вызвав clearInterval()
.
Мы создаем компонент, который вызывает функцию обратного вызова, чтобы указать, что он завершился после x
циклов. В этом примере я использую React, но он работает с любой платформой FE.
импортировать React, { useRef } из «реагировать»; const Timer = ({ cicles, onFinish }) => { const currentCicles = useRef(0); setInterval(() => { if (currentCicles.current >= cicles) { ОнГотово(); возвращаться; } currentCicles.current++; }, 500); возвращаться ( <p>Загрузка...</p> ); } экспортировать таймер по умолчанию;
На первый взгляд кажется, что никаких проблем нет. Не волнуйтесь, давайте создадим еще один компонент, который запускает этот таймер, и проанализируем его производительность памяти.
импортировать React, {useState} из «реагировать»; импортировать стили из «../styles/Home.module.css» импортировать таймер из «../компоненты/Таймер»; экспортировать функцию по умолчанию Home() { const [showTimer, setShowTimer] = useState(); const onFinish = () => setShowTimer(false); возвращаться ( <p className={styles.container}> {шоуТаймер ? ( <Timer cicles={10} onFinish={onFinish} /> ): ( <button onClick={() => setShowTimer(true)}> Повторить попытку </кнопка> )} </p> ) }
После нескольких нажатий кнопки Retry
» вы увидите результат использования Chrome Dev Tools для определения использования памяти:
Когда мы нажимаем кнопку повтора, мы видим, что выделяется все больше и больше памяти. Это означает, что ранее выделенная память не была освобождена. Таймер все еще работает, а не заменяется.
Как решить эту проблему? Возвращаемое значение setInterval
— это идентификатор интервала, который мы можем использовать для отмены этого интервала. В данном конкретном случае мы можем clearInterval
после выгрузки компонента.
useEffect(() => { const интервалId = setInterval(() => { if (currentCicles.current >= cicles) { ОнГотово(); возвращаться; } currentCicles.current++; }, 500); return () => ClearInterval(intervalId); }, [])
Иногда при написании кода сложно обнаружить эту проблему. Лучший способ — абстрагировать компоненты.
Используя здесь React, мы можем обернуть всю эту логику в собственный хук.
импортировать { useEffect } из «реагировать»; экспортировать const useTimeout = (refreshCycle = 100, обратный вызов) => { useEffect(() => { если (refreshCycle <= 0) { setTimeout (обратный вызов, 0); возвращаться; } const интервалId = setInterval(() => { перезвонить(); }, обновитьЦикл); return () => ClearInterval(intervalId); }, [refreshCycle, setInterval,clearInterval]); }; экспортировать useTimeout по умолчанию;
Теперь всякий раз, когда вам нужно использовать setInterval
, вы можете сделать это:
const handleTimeout = () => ...; useTimeout (100, handleTimeout);
Теперь вы можете использовать этот useTimeout Hook
, не беспокоясь об утечках памяти, что также является преимуществом абстракции.
Веб-API предоставляет большое количество прослушивателей событий. Ранее мы обсуждали setTimeout
. Теперь давайте посмотрим на addEventListener
.
В этом примере мы создаем функцию сочетания клавиш. Поскольку на разных страницах у нас разные функции, будут созданы разные функции сочетаний клавиш.
функция homeShortcuts({ключ}) { если (ключ === 'E') { console.log('редактировать виджет') } } // Когда пользователь входит в систему на главной странице, мы выполняем document.addEventListener('keyup', homeShortcuts); // Пользователь что-то делает, а затем переходит к настройкам function settingsShortcuts({key}) { если (ключ === 'E') { console.log('изменить настройку') } } // Когда пользователь входит на домашнюю страницу, мы выполняем document.addEventListener('keyup', settingsShortcuts);
Он по-прежнему выглядит нормально, за исключением того, что предыдущая keyup
не очищается при выполнении второго addEventListener
. Вместо замены нашего прослушивателя keyup
этот код добавит еще один callback
. Это означает, что при нажатии клавиши активируются две функции.
Чтобы очистить предыдущий обратный вызов, нам нужно использовать removeEventListener
:
document.removeEventListener('keyup', homeShortcuts);
Рефакторинг приведенного выше кода:
функция homeShortcuts({ключ}) { если (ключ === 'E') { console.log('редактировать виджет') } } // пользователь попадает домой, и мы выполняем document.addEventListener('keyup', homeShortcuts); // пользователь делает что-то и переходит к настройкам настройки функцииShortcuts({key}) { если (ключ === 'E') { console.log('изменить настройку') } } // пользователь попадает домой, и мы выполняем document.removeEventListener('keyup', homeShortcuts); document.addEventListener('keyup', settingsShortcuts);
Как правило, будьте очень осторожны при использовании инструментов из глобальных объектов.
Наблюдатели — это функция веб-API браузера, о которой многие разработчики не знают. Это полезно, если вы хотите проверить изменения видимости или размера HTML-элементов.
Интерфейс IntersectionObserver
(часть API Intersection Observer) предоставляет метод для асинхронного наблюдения за состоянием пересечения целевого элемента с его родительскими элементами или viewport
документа верхнего уровня. Элемент-предок и viewport
называются root
.
Несмотря на то, что он мощный, мы должны использовать его с осторожностью. Закончив наблюдение за объектом, не забудьте отменить его, когда он не используется.
Взгляните на код:
константная ссылка = ... const видимый = (видимый) => { console.log(`Это ${visible}`); } useEffect(() => { если (!ref) { возвращаться; } наблюдатель.текущий = новый IntersectionObserver( (записи) => { если (!entries[0].isIntersecting) { видимый (истина); } еще { видимый (ложь); } }, { rootMargin: `-${header.height}px` }, ); наблюдатель.текущий.наблюдать(ссылка); }, [ссылка]);
Код выше выглядит нормально. Однако что происходит с наблюдателем, когда компонент выгружается и происходит утечка памяти? Как нам решить эту проблему? Просто используйте метод disconnect
:
константная ссылка = ... const видимый = (видимый) => { console.log(`Это ${visible}`); } useEffect(() => { если (!ref) { возвращаться; } наблюдатель.текущий = новый IntersectionObserver( (записи) => { если (!entries[0].isIntersecting) { видимый (истина); } еще { видимый (ложь); } }, { rootMargin: `-${header.height}px` }, ); наблюдатель.текущий.наблюдать(ссылка); return () => Observer.current?.disconnect(); }, [ссылка]);
Добавление объектов в окно — распространенная ошибка. В некоторых сценариях его может быть сложно найти, особенно при использовании ключевого слова this
в контексте выполнения окна. Взгляните на следующий пример:
функция addElement(элемент) { если (!this.stack) { this.stack = { элементы: [] } } this.stack.elements.push(элемент); }
Это выглядит безобидно, но это зависит от того, из какого контекста вы вызываете addElement
. Если вы вызовете addElement из контекста окна, куча будет расти.
Другая проблема может заключаться в неправильном определении глобальной переменной:
var a = 'example 1'; // Область действия ограничена местом создания var b = 'example 2'; // Добавлено в объект Window;
Чтобы предотвратить эту проблему, вы можете использовать строгий режим:
"используйте строгий"
Используя строгий режим, вы сигнализируете компилятору JavaScript, что хотите защитить себя от такого поведения. Вы по-прежнему можете использовать Window, когда вам нужно. Однако вы должны использовать его явным образом.
Как строгий режим влияет на наш предыдущий пример:
Для функции addElement
this
значение не определено при вызове из глобальной области видимости.
Если вы не укажете const | let | var
для переменной, вы получите следующую ошибку:
Неперехваченная ошибка ссылки: b не определен
Узлы DOM также не застрахованы от утечек памяти. Нам нужно быть осторожными и не сохранять ссылки на них. В противном случае сборщик мусора не сможет их очистить, поскольку они все равно будут доступны.
Продемонстрируйте это с помощью небольшого фрагмента кода:
константные элементы = []; const list = document.getElementById('list'); функция addElement() { // очищаем узлы list.innerHTML = ''; const pElement = document.createElement('p'); const element = document.createTextNode(`добавляем элемент ${elements.length}`); pElement.appendChild(элемент); list.appendChild(pElement); elements.push(pElement); } document.getElementById('addElement').onclick = addElement;
Обратите внимание, что функция addElement
очищает список p
и добавляет в него новый элемент в качестве дочернего элемента. Этот вновь созданный элемент добавляется в массив elements
.
При следующем выполнении addElement
элемент будет удален из списка p
, но он не подходит для сборки мусора, поскольку хранится в массиве elements
.
Мы отслеживаем функцию после ее выполнения несколько раз:
Посмотрите, как узел был скомпрометирован, на скриншоте выше. Так как решить эту проблему? Очистка массива elements
сделает их пригодными для сбора мусора.
В этой статье мы рассмотрели наиболее распространенные способы утечек памяти. Очевидно, что сам JavaScript не допускает утечки памяти. Вместо этого это вызвано непреднамеренным сохранением памяти со стороны разработчика. Пока код чист и мы не забываем за собой убирать, утечек не будет.
Понимание того, как в JavaScript работают память и сбор мусора, является обязательным. У некоторых разработчиков возникает ложное ощущение, что, поскольку это происходит автоматически, им не нужно беспокоиться об этой проблеме.