Управление памятью в JavaScript с помощью WeakRef и FinalizationRegistry
Привет, Хабр!
Сегодня мы рассмотрим тему управления памятью в JavaScript — и речь пойдет не о классическом сборщике мусора, а о возможностях с WeakRef и FinalizationRegistry. Эти инструменты помогают работать со слабыми ссылками и асинхронной финализацией объектов, открывая дорогу к более тонкой работе с памятью.
Если вам надоело, что объекты висят в памяти дольше, чем нужно, и хочется управлять ресурсами без лишних утечек — эта статья для вас. Начнем!
Обзор синтаксиса WeakRef и FinalizationRegistry
WeakRef
Итак, начнем с WeakRef. Это довольно свежая фича JavaScript (ES2021), которая позволяет создать «слабую» ссылку на объект, что означает: если этот объект больше нигде не используется, GC его может удалить, не дожидаясь, пока все ссылки будут явно сброшены.
Создание слабой ссылки — это супер просто:
const weakRef = new WeakRef(targetObject);
Где targetObject
— это любой объект, на который ты хочешь создать слабую ссылку.
Но на этом все не заканчивается, потому что WeakRef открывает важный нюанс: слабая ссылка не защищает объект от удаления сборщиком мусора. Т.е ты можешь создать ссылку, но GC не пощадит твой объект, если тот больше не нужен. Поэтому WeakRef идеален для кейсов вроде кэширования, когда ты хочешь держать объект, пока он используется, но если он не нужен — его стоит обсвободить.
Чтобы достать объект из слабой ссылки, есть метод deref()
:
const obj = weakRef.deref();
Метод deref()
вернет объект, если тот еще существует. Если сборщик мусора уже прибрал его к рукам, то deref()
вернет undefined
. Это и есть главный прикол слабых ссылок — нельзя быть увереным, что объект еще в памяти.
Вот пример:
let targetObject = { name: "Weak object" };
let weakRef = new WeakRef(targetObject);
// В какой-то момент...
targetObject = null; // Теперь объект доступен только через WeakRef
let obj = weakRef.deref();
if (obj) {
console.log(`Объект все еще существует: ${obj.name}`);
} else {
console.log("Объект был удален сборщиком мусора.");
}
Подводные камни
Проблема с синхронизацией: Если вы подумали, что можно быть суперумным и создать много слабых ссылок на один объект, имейте в виду: после удаления объекта WeakRef не дает гарантии, что его всегда можно будет восстановить. Поэтому при кэшировании или других задачах нужно проверять, что объект все еще существует после вызова
deref()
.Использование с осторожностью: WeakRef может быть полезен, но не злоупотребляй им.
FinalizationRegistry
Теперь поговорим про FinalizationRegistry.Это реестр, который позволяет отслеживать, когда объекты становятся недоступными и освобождать ресурсы асинхронно, через коллбеки.
Сначала создадим реестр:
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Объект был финализирован: ${heldValue}`);
});
Здесь коллбек heldValue
будет вызван, когда объект станет недоступным для кода, и JavaScript его удалит. Это очень хорошо для освобождения внешних ресурсов, таких как файловые дескрипторы или сокеты.
Теперь, чтобы зарегистрировать объект в этом реестре:
registry.register(targetObject, heldValue, unregisterToken);
targetObject
: сам объект, который ты отслеживаешь.heldValue
: значение, которое будет передано в коллбек, когда объект будет удален.unregisterToken
: это опциональный параметр, который позволяет отписаться от отслеживания объекта.
Вот как это выглядит на практике:
let targetObject = { name: "Tracked object" };
const registry = new FinalizationRegistry((heldValue) => {
console.log(`Объект был финализирован: ${heldValue}`);
});
// Регистрируем объект
registry.register(targetObject, "Мой объект");
// Где-то в коде...
targetObject = null; // Теперь объект доступен только через реестр
Когда объект будет удален сборщиком мусора, коллбек вызовется с переданным значением heldValue
.
Если нужно отменить регистрацию объекта, то просто вызываешь:
registry.unregister(unregisterToken);
Этот unregisterToken
— просто уникальный идентификатор для каждого объекта, который ты передал при регистрации.
Коллбек финализации работает асинхронно, так что будь готов, что освобождение ресурсов может произойти в неопределенное время.
Применение
WeakRef для кэширования
WeakRef — это идеальный инструмент для создания кэша, который автоматически освобождает память, если объект больше не нужен. Представь, что ты делаешь веб-приложение, которое загружает кучу данных из API, но не хочешь, чтобы эти данные висели в памяти вечно. Кэш с WeakRef позволит тебе держать объект в памяти, но при этом не даст ему захламлять память, если он больше не используется.
Пример:
class Cache {
constructor() {
this.cache = new Map();
}
set(key, value) {
// Создаем слабую ссылку на объект
this.cache.set(key, new WeakRef(value));
}
get(key) {
const weakRef = this.cache.get(key);
if (weakRef) {
// Получаем объект из слабой ссылки
const obj = weakRef.deref();
if (obj) {
console.log(`Объект по ключу "${key}" найден в кэше.`);
return obj;
} else {
console.log(`Объект по ключу "${key}" был удален сборщиком мусора.`);
this.cache.delete(key); // Очищаем кэш, если объект был удален
}
} else {
console.log(`Ключ "${key}" не найден в кэше.`);
}
return null;
}
}
// Пример использования:
const cache = new Cache();
let userData = { name: "Alice", age: 30 };
cache.set("user_1", userData);
// Принудительно освобождаем объект
userData = null;
// Пробуем получить объект через кэш
setTimeout(() => {
const cachedData = cache.get("user_1");
if (cachedData) {
console.log(`Данные из кэша: ${cachedData.name}, ${cachedData.age}`);
} else {
console.log("Данные были удалены сборщиком мусора.");
}
}, 1000);
Мы создаем кэш, который хранит слабые ссылки на объекты. Если объект больше не нужен, GC удалит его из памяти, а наш кэш сам обновится. При следующей попытке обращения мы сможем понять, был ли объект удален, и при необходимости подгрузить его заново.
Обработка DOM элементов с WeakRef
Еще один классный случай для WeakRef — это работа с DOM элементами, которые могут появляться и исчезать. Допусти, нужно создать приложение SPA, где некоторые компоненты могут быть временно удалены из DOM. Используя слабые ссылки, можно кэшировать информацию о DOM элементах, не опасаясь, что они останутся в памяти после удаления из документа.
Пример:
class DomCache {
constructor() {
this.domElements = new Map();
}
setElement(id, element) {
this.domElements.set(id, new WeakRef(element));
}
getElement(id) {
const weakRef = this.domElements.get(id);
if (weakRef) {
const element = weakRef.deref();
if (element) {
console.log(`Элемент с ID "${id}" найден в кэше.`);
return element;
} else {
console.log(`Элемент с ID "${id}" был удален сборщиком мусора.`);
this.domElements.delete(id); // Удаляем из кэша
}
} else {
console.log(`Элемент с ID "${id}" не найден.`);
}
return null;
}
}
// Пример использования:
const domCache = new DomCache();
const divElement = document.createElement("div");
divElement.id = "myDiv";
document.body.appendChild(divElement);
domCache.setElement("myDiv", divElement);
// Удаляем элемент из DOM
document.body.removeChild(divElement);
// Пробуем получить элемент через WeakRef
setTimeout(() => {
const cachedElement = domCache.getElement("myDiv");
if (cachedElement) {
console.log("Элемент найден и все еще существует.");
} else {
console.log("Элемент был удален сборщиком мусора.");
}
}, 1000);
Храним ссылку на DOM элемент в кэше, используя WeakRef. Когда элемент удаляется из DOM, он также может быть удален сборщиком мусора, и мы сможем это отследить.
Освобождение ресурсов с FinalizationRegistry
Теперь переходим к реестрам. FinalizationRegistry идеально подходит для задач, где нужно освободить ресурсы: закрытие файлов, соединений или выполнение других операций, когда объект становится недоступным.
Пример:
class FileManager {
constructor() {
this.registry = new FinalizationRegistry((fileName) => {
console.log(`Освобождаем ресурсы для файла: ${fileName}`);
});
}
openFile(fileName) {
const fileObject = { name: fileName };
this.registry.register(fileObject, fileName);
return fileObject;
}
}
// Пример использования:
const fileManager = new FileManager();
let file = fileManager.openFile("myfile.txt");
// Освобождаем ссылку на файл
file = null;
// Когда сборщик мусора удалит объект, вызовется коллбек и освободит ресурсы.
Создали файл, зарегистрировали его в FinalizationRegistry, а когда объект стал недоступным, система освободила связанные с ним ресурсы.
Очистка кэша с FinalizationRegistry
Один из моих любимых сценариев — это очистка кэша после удаления объекта.
Пример:
class ObjectCache {
constructor() {
this.cache = new Map();
this.registry = new FinalizationRegistry((key) => {
console.log(`Объект с ключом "${key}" был удален. Очищаем кэш.`);
this.cache.delete(key);
});
}
setObject(key, obj) {
this.cache.set(key, obj);
this.registry.register(obj, key);
}
getObject(key) {
return this.cache.get(key);
}
}
// Пример использования:
const cache = new ObjectCache();
let obj = { name: "Cache me if you can" };
cache.setObject("obj_1", obj);
// Освобождаем ссылку
obj = null;
// Когда объект будет удален сборщиком мусора, кэш будет автоматически очищен.
Создали кэш и зарегистрировали объекты в FinalizationRegistry. Когда объект становится недоступным, реестр заботится о том, чтобы удалить его из кэша.
Заключение
Вот так обстоят дела с WeakRef и FinalizationRegistry. Если использовать их грамотно, можно существенно улучшить управление памятью и избежать утечек в сложных приложениях. Кэширование, работа с DOM элементами, освобождение файловых дескрипторов и сетевых соединений — и не только, все это теперь находится под твоим контролем.
Ну что, пора применить эти штуки в продакшене?
В завершение скажу пару слов об открытом уроке, посвящённом созданию RestFull API с NestJs, который пройдет 24 сентября. В результате участия в нём научитесь создавать масштабированое API при помощи современных фреймворков. Если интересно — записывайтесь по ссылке.