Изучение слабых API JavaScript

Процесс управления памятью в JavaScript на первый взгляд кажется простым. Память управляется виртуальной машиной JavaScript через процесс восстановления памяти сборки мусора.

Однако мы не можем заставить сборку мусора работать программно. Браузер сам решает, когда пришло время для его выполнения. Поскольку для этого нет общих спецификаций, они будут варьироваться от одного поставщика к другому. Это непредсказуемый и недетерминированный процесс.

Это не значит, что мы можем не беспокоиться об этом. Нам нужно понять, как работает память под крючком. В противном случае мы можем получить несколько утечек памяти. Чтобы иметь право на сборку мусора, объект не должен упоминаться где-либо еще. Он должен быть недосягаем. Напротив, при сохранении ссылки на объект этот ресурс не может быть освобожден.

Что происходит, когда мы получаем ссылку от объекта, которым мы не управляем? Должны ли мы сохранить ссылку? Должны ли мы просто скопировать объект? Как мы можем узнать, когда на него больше не ссылаются?

Слабые API-интерфейсы JavaScript были созданы с этой мотивацией. Это позволяет нам хранить ссылки на объекты, не мешая процессу сборки мусора. Целевой объект может быть восстановлен в любое время. Это не будет добавлением сильной ссылки. Это подходит, когда мы хотим хранить информацию об объекте, пока он жив.

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

API WeakRef — ES2021

Этот WeakRef API является одним из новейших слабых API. Это позволяет нам сохранять слабую ссылку на объект и извлекать ее, когда она нам нужна.

Как это использовать? Это двухэтапный процесс.

1. Создание слабой ссылки: целевой параметр должен быть объектом. Только те, которые собираются мусором. Например, вы не можете создать WeakRef для строки или числа.

// creating a weak object reference
const ElmentRef = new WeakRef(element);

2. Вытягивание ссылки: чтобы использовать цель, на которую указывает ссылка, мы должны использовать deref(). Первый шаг просто генерирует обычный объект JavaScript, который содержит слабую ссылку на нашу цель. Мы мало что можем с этим сделать.

// pulling stored weak object
const element = ElmentRef.deref();

Как только мы используем этот метод deref(), мы создаем новую сильную ссылку на объект. Это означает, что этот объект больше не подлежит сборке мусора, пока эта ссылка не станет неиспользуемой. Было бы бессмысленно, если бы этот объект мог стать undefined. Это сделало бы функцию ненадежной и бесполезной.

Давайте посмотрим полный пример:

Обратите внимание, что в приведенном выше коде нам всегда нужно проверять, что возврат из deref не равен undefined. Ресурс можно было бы вернуть в любое время, если бы элемент DOM был удален из DOM.

Давайте посмотрим на другой пример. Давайте сделаем трекер памяти элементов:

В приведенном выше коде мы будем продолжать получать операторы консоли Element is still on memory до тех пор, пока элемент не будет стерт из памяти при сборке мусора.

API WeakSet и WeakMap — ES2015

Это самые старые слабые API Javascript. Они существуют с ES6 года. Несмотря на то, что они могут содержать набор слабых ссылок, они не являются итерируемыми. Мы можем только добавлять значения, запрашивать их наличие или удалять.

Мы взаимодействуем с ними всегда через целевой объект.

Давайте посмотрим на пример WeakSet. Он прост, он служит только слабым словарем.

WeakMap более сложный. Мы можем использовать его для хранения дополнительной информации о целевом объекте. Это могут быть объекты или просто примитивы. Они будут утилизированы, когда целевая ссылка будет удалена сборщиком мусора.

Давайте посмотрим на пример:

Как видно из приведенного выше примера, при добавлении значения через set() его можно получить, выполнив метод get() с той же целью.

Финализатор API — ES2021

В последней версии ES2021 есть способ узнать, был ли объект утилизирован сборщиком мусора. Эта функция называется Finalizers.

Однако:

  • Нет никакой гарантии, что обратный вызов будет выполнен.
  • Целевой объект уже очищен и недоступен.
  • Как долго будет выполняться обратный вызов, не является детерминированным. Это может быть одна минута или один час.
  • Поведение может различаться между поставщиками

Почему метод register принимает два значения? Когда вызывается FinalizationRegistry, значение x уже будет удалено сборщиком мусора. Таким образом, не было бы никакого способа вернуть это значение. Вместо этого мы можем передать любую целевую подсказку, которую считаем подходящей.

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

// ❌ x would never be reclaimed as it is strongly held in the second parameter
cleanup.register(x, x);

Существует способ отменить регистрацию выполнения обратного вызова. Метод может принимать третий параметр, который не является строго фиксированным.

// ✅ x is not strongly held so it can be garbage collected
cleanup.register(x, 'cleaning up x', x);
// ✅ will remove callback
cleanup.unregister(x);

Не рекомендуется использовать эту функцию в критической области. Он просто предназначен для дополнительной очистки, чтобы еще больше уменьшить объем памяти, занимаемой нашим веб-приложением.

Случаи использования

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

1. Дополнительные данные

Иногда мы просто хотим улучшить некоторые объекты, которые не принадлежат нам. Мы не контролируем их ссылку или их поток.

В качестве альтернативы мы можем изменить экземпляр Object, что нарушит парадигму функционального программирования. Создание другого объекта с некоторыми изменениями увеличило бы наш объем памяти.

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

2. Кэширование

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

Другим примером является объединение неизменности с WeakMap для создания кеша для хранения результатов ресурсоемкой операции:

Это означает, что мы получаем автоматически очищаемый кеш, встроенный в движок JavaScript. Это может стать весьма полезным.

3. Дополнительные очистки

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

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

Резюме

Мы видели, как JavaScript недавно улучшил свои слабые функции ссылок. Мы внезапно получаем больше контроля над нашим приложением и располагаемся ближе к движку.

Как упоминалось ранее, важно не слишком полагаться на какой-либо слабый API-интерфейс движка. У них есть довольно специфические варианты использования. Процесс сборки мусора сложен и будет отличаться от браузера к браузеру. Это сложная тема, которая постоянно совершенствуется и подвержена изменениям.

Вам интересно узнать больше об утечках памяти в JavaScript? Проверьте мою статью ниже: