Представь редактор изображений в браузере. Пользователь открыл 200 фотографий, каждая по 5 МБ. Кэшировать декодированные данные удобно — но если держать все 200 в памяти, браузер упадёт. Нужен кэш, который сам освобождает память когда она нужна системе. Это и есть задача WeakRef.
Обычный Map держит объекты в памяти вечно — GC не может их удалить. WeakRef позволяет создать ссылку на объект, которая не мешает GC. Объект живёт пока кто-то другой держит сильную ссылку — но как только они кончаются, GC вправе его удалить.
Обычная ссылка на объект называется сильной — пока она существует, GC не удалит объект:
let obj = { name: 'Данные' } // сильная ссылка
const ref = obj // ещё одна сильная ссылка
obj = null // убрали первую ссылку
// Объект НЕ удалён — ref всё ещё держит его в памятиСлабая ссылка (WeakRef) не препятствует удалению объекта GC:
let obj = { name: 'Данные' }
const weakRef = new WeakRef(obj) // слабая ссылка
obj = null // убрали сильную ссылку
// GC может удалить объект в любой момент!
// weakRef.deref() вернёт объект ИЛИ undefined (если уже удалён)
const current = weakRef.deref()
if (current !== undefined) {
console.log(current.name) // объект ещё жив
} else {
console.log('Объект был удалён GC')
}Главная задача WeakRef — кэш, который не удерживает объекты дольше необходимого:
class WeakCache {
constructor() {
this._cache = new Map() // Map<key, WeakRef<value>>
}
set(key, value) {
this._cache.set(key, new WeakRef(value))
}
get(key) {
const ref = this._cache.get(key)
if (!ref) return undefined
const value = ref.deref()
if (value === undefined) {
// Объект удалён GC — очищаем запись
this._cache.delete(key)
return undefined
}
return value
}
has(key) {
return this.get(key) !== undefined
}
}Позволяет зарегистрировать callback, который будет вызван после того, как GC удалит объект:
const registry = new FinalizationRegistry((heldValue) => {
// heldValue — данные переданные при регистрации
console.log(`Объект "${heldValue}" был удалён GC`)
// Здесь можно, например, очистить внешний кэш или освободить ресурс
})
let user = { id: 1, name: 'Алиса' }
registry.register(user, 'пользователь Алиса')
// При регистрации передаём heldValue — не сам объект, а метаданные!
user = null // GC может удалить объект, тогда вызовется callbackВажно: callback вызывается недетерминированно — нельзя предсказать когда именно GC запустится.
GC не запускается по расписанию. Он включается когда движку нужно освободить память:
// ПЛОХО — нельзя полагаться на момент вызова FinalizationRegistry
const reg = new FinalizationRegistry((key) => {
freeResource(key) // может вызваться через секунду или через час
})
// ХОРОШО — для предсказуемой очистки используй явный dispose
class Resource {
constructor() {
this._open = true
reg.register(this, 'resource-1') // страховка на случай если забудут dispose
}
dispose() {
if (this._open) {
this._open = false
freeResource() // явная детерминированная очистка
}
}
}| | WeakRef | WeakMap | WeakSet |
|---|---|---|---|
| Хранит | Одну слабую ссылку | Пары ключ→значение | Набор объектов |
| Доступ к объекту | deref() — может вернуть undefined | get(key) | has(obj) |
| Итерация | Нет | Нет | Нет |
| Когда использовать | Кэш | Метаданные к объектам | Маркировка объектов |
// WeakMap — привязать приватные данные к объекту без удержания
const privateData = new WeakMap()
class User {
constructor(name, password) {
privateData.set(this, { password }) // не виден снаружи
this.name = name
}
checkPassword(pwd) {
return privateData.get(this).password === pwd
}
}
// Когда объект User удалён GC — WeakMap автоматически удаляет и запись1. Хранить примитивы в WeakRef — нельзя, только объекты
// ПЛОХО — TypeError: WeakRef принимает только объекты
const ref = new WeakRef(42) // TypeError
const ref2 = new WeakRef('строка') // TypeError
// ХОРОШО — только объекты (включая массивы и функции)
const ref3 = new WeakRef({ value: 42 })
const ref4 = new WeakRef([1, 2, 3])2. Вызвать deref() один раз и кэшировать результат
// ПЛОХО — объект могут удалить между двумя обращениями
const ref = new WeakRef(someObject)
const obj = ref.deref()
// ... время прошло, GC удалил объект ...
if (obj) {
obj.method() // может быть, obj уже удалён!
}
// ХОРОШО — каждый раз вызывай deref() и проверяй
function useWeakRef(ref) {
const obj = ref.deref()
if (!obj) return // объект мёртв
obj.method() // безопасно в этом синхронном контексте
}3. Строить критичную логику на FinalizationRegistry
// ПЛОХО — нельзя полагаться на своевременную очистку
const registry = new FinalizationRegistry((connectionId) => {
closeDbConnection(connectionId) // может вызваться через секунды или никогда в тесте
})
// ХОРОШО — явный dispose + registry как страховка
class DbConnection {
constructor(id) {
this._id = id
this._closed = false
registry.register(this, id)
}
dispose() {
if (!this._closed) { this._closed = true; closeDbConnection(this._id) }
}
}@vue/reactivity внутри использует WeakRef для отслеживания зависимостей компонентовWeakRef-кэш (концептуальная демонстрация) и практичная реализация через Map с TTL
// WeakRef-based кэш — концептуальная демонстрация
// В реальном браузере GC может удалить объекты, deref() вернёт undefined
class WeakRefCache {
constructor() {
this._store = new Map() // Map<string, WeakRef<object>>
}
set(key, value) {
this._store.set(key, new WeakRef(value))
}
get(key) {
const ref = this._store.get(key)
if (!ref) return undefined
const value = ref.deref()
if (value === undefined) {
// GC удалил объект — убираем мёртвую запись
this._store.delete(key)
console.log(`[WeakRefCache] GC удалил запись "${key}"`)
}
return value
}
has(key) {
return this.get(key) !== undefined
}
get size() {
return this._store.size // может быть больше живых объектов!
}
}
// Демонстрация концепции
console.log('=== WeakRef концепция ===')
const weakCache = new WeakRefCache()
let userData = { id: 1, name: 'Алиса Петрова', role: 'admin' }
weakCache.set('user:1', userData)
console.log('После set — объект жив:', weakCache.get('user:1')?.name) // 'Алиса Петрова'
// В реальном коде: userData = null → GC может удалить объект
// После этого weakCache.get('user:1') вернёт undefined
console.log('Пока userData существует:', weakCache.has('user:1')) // true
// ===
// Практичная альтернатива: кэш с TTL (Time-To-Live)
// Поскольку GC недетерминирован, в реальных приложениях чаще используют TTL-кэш
class TTLCache {
constructor(defaultTtlMs = 60000) {
this._store = new Map()
this._defaultTtl = defaultTtlMs
}
set(key, value, ttlMs = this._defaultTtl) {
this._store.set(key, {
value,
expiresAt: Date.now() + ttlMs,
})
}
get(key) {
const entry = this._store.get(key)
if (!entry) return undefined
if (Date.now() > entry.expiresAt) {
this._store.delete(key)
return undefined // запись истекла
}
return entry.value
}
has(key) {
return this.get(key) !== undefined
}
// Удалить все истёкшие записи (можно вызывать периодически)
cleanup() {
const now = Date.now()
let removed = 0
for (const [key, entry] of this._store) {
if (now > entry.expiresAt) {
this._store.delete(key)
removed++
}
}
return removed
}
get size() { return this._store.size }
}
console.log('\n=== TTL Cache демонстрация ===')
const cache = new TTLCache(500) // TTL 500ms для демонстрации
// Кэшируем результаты вычислений
function expensiveCalc(n) {
const cacheKey = `calc:${n}`
const cached = cache.get(cacheKey)
if (cached !== undefined) {
console.log(`[${cacheKey}] из кэша: ${cached}`)
return cached
}
// "Дорогое" вычисление
const result = Array.from({ length: n }, (_, i) => i + 1).reduce((a, b) => a + b, 0)
cache.set(cacheKey, result)
console.log(`[${cacheKey}] вычислено и сохранено: ${result}`)
return result
}
expensiveCalc(100) // вычислено
expensiveCalc(100) // из кэша
expensiveCalc(200) // вычислено
expensiveCalc(200) // из кэша
console.log('Размер кэша:', cache.size) // 2
// Симулируем истечение TTL
console.log('\n=== FinalizationRegistry паттерн (концептуально) ===')
function createFinalizationRegistryDemo() {
const log = []
// В реальном коде: new FinalizationRegistry(heldValue => { ... })
// Здесь показываем паттерн без реального GC
const registry = {
callbacks: new Map(),
register(target, heldValue) {
log.push(`Зарегистрирован: "${heldValue}"`)
},
// В реальности этот метод вызывается GC автоматически
simulateGC(heldValue) {
log.push(`GC удалил объект → callback для "${heldValue}"`)
}
}
return { registry, log }
}
const { registry, log } = createFinalizationRegistryDemo()
registry.register({}, 'пользователь-42')
registry.register({}, 'изображение-banner.png')
registry.simulateGC('изображение-banner.png')
log.forEach(entry => console.log(entry))Представь редактор изображений в браузере. Пользователь открыл 200 фотографий, каждая по 5 МБ. Кэшировать декодированные данные удобно — но если держать все 200 в памяти, браузер упадёт. Нужен кэш, который сам освобождает память когда она нужна системе. Это и есть задача WeakRef.
Обычный Map держит объекты в памяти вечно — GC не может их удалить. WeakRef позволяет создать ссылку на объект, которая не мешает GC. Объект живёт пока кто-то другой держит сильную ссылку — но как только они кончаются, GC вправе его удалить.
Обычная ссылка на объект называется сильной — пока она существует, GC не удалит объект:
let obj = { name: 'Данные' } // сильная ссылка
const ref = obj // ещё одна сильная ссылка
obj = null // убрали первую ссылку
// Объект НЕ удалён — ref всё ещё держит его в памятиСлабая ссылка (WeakRef) не препятствует удалению объекта GC:
let obj = { name: 'Данные' }
const weakRef = new WeakRef(obj) // слабая ссылка
obj = null // убрали сильную ссылку
// GC может удалить объект в любой момент!
// weakRef.deref() вернёт объект ИЛИ undefined (если уже удалён)
const current = weakRef.deref()
if (current !== undefined) {
console.log(current.name) // объект ещё жив
} else {
console.log('Объект был удалён GC')
}Главная задача WeakRef — кэш, который не удерживает объекты дольше необходимого:
class WeakCache {
constructor() {
this._cache = new Map() // Map<key, WeakRef<value>>
}
set(key, value) {
this._cache.set(key, new WeakRef(value))
}
get(key) {
const ref = this._cache.get(key)
if (!ref) return undefined
const value = ref.deref()
if (value === undefined) {
// Объект удалён GC — очищаем запись
this._cache.delete(key)
return undefined
}
return value
}
has(key) {
return this.get(key) !== undefined
}
}Позволяет зарегистрировать callback, который будет вызван после того, как GC удалит объект:
const registry = new FinalizationRegistry((heldValue) => {
// heldValue — данные переданные при регистрации
console.log(`Объект "${heldValue}" был удалён GC`)
// Здесь можно, например, очистить внешний кэш или освободить ресурс
})
let user = { id: 1, name: 'Алиса' }
registry.register(user, 'пользователь Алиса')
// При регистрации передаём heldValue — не сам объект, а метаданные!
user = null // GC может удалить объект, тогда вызовется callbackВажно: callback вызывается недетерминированно — нельзя предсказать когда именно GC запустится.
GC не запускается по расписанию. Он включается когда движку нужно освободить память:
// ПЛОХО — нельзя полагаться на момент вызова FinalizationRegistry
const reg = new FinalizationRegistry((key) => {
freeResource(key) // может вызваться через секунду или через час
})
// ХОРОШО — для предсказуемой очистки используй явный dispose
class Resource {
constructor() {
this._open = true
reg.register(this, 'resource-1') // страховка на случай если забудут dispose
}
dispose() {
if (this._open) {
this._open = false
freeResource() // явная детерминированная очистка
}
}
}| | WeakRef | WeakMap | WeakSet |
|---|---|---|---|
| Хранит | Одну слабую ссылку | Пары ключ→значение | Набор объектов |
| Доступ к объекту | deref() — может вернуть undefined | get(key) | has(obj) |
| Итерация | Нет | Нет | Нет |
| Когда использовать | Кэш | Метаданные к объектам | Маркировка объектов |
// WeakMap — привязать приватные данные к объекту без удержания
const privateData = new WeakMap()
class User {
constructor(name, password) {
privateData.set(this, { password }) // не виден снаружи
this.name = name
}
checkPassword(pwd) {
return privateData.get(this).password === pwd
}
}
// Когда объект User удалён GC — WeakMap автоматически удаляет и запись1. Хранить примитивы в WeakRef — нельзя, только объекты
// ПЛОХО — TypeError: WeakRef принимает только объекты
const ref = new WeakRef(42) // TypeError
const ref2 = new WeakRef('строка') // TypeError
// ХОРОШО — только объекты (включая массивы и функции)
const ref3 = new WeakRef({ value: 42 })
const ref4 = new WeakRef([1, 2, 3])2. Вызвать deref() один раз и кэшировать результат
// ПЛОХО — объект могут удалить между двумя обращениями
const ref = new WeakRef(someObject)
const obj = ref.deref()
// ... время прошло, GC удалил объект ...
if (obj) {
obj.method() // может быть, obj уже удалён!
}
// ХОРОШО — каждый раз вызывай deref() и проверяй
function useWeakRef(ref) {
const obj = ref.deref()
if (!obj) return // объект мёртв
obj.method() // безопасно в этом синхронном контексте
}3. Строить критичную логику на FinalizationRegistry
// ПЛОХО — нельзя полагаться на своевременную очистку
const registry = new FinalizationRegistry((connectionId) => {
closeDbConnection(connectionId) // может вызваться через секунды или никогда в тесте
})
// ХОРОШО — явный dispose + registry как страховка
class DbConnection {
constructor(id) {
this._id = id
this._closed = false
registry.register(this, id)
}
dispose() {
if (!this._closed) { this._closed = true; closeDbConnection(this._id) }
}
}@vue/reactivity внутри использует WeakRef для отслеживания зависимостей компонентовWeakRef-кэш (концептуальная демонстрация) и практичная реализация через Map с TTL
// WeakRef-based кэш — концептуальная демонстрация
// В реальном браузере GC может удалить объекты, deref() вернёт undefined
class WeakRefCache {
constructor() {
this._store = new Map() // Map<string, WeakRef<object>>
}
set(key, value) {
this._store.set(key, new WeakRef(value))
}
get(key) {
const ref = this._store.get(key)
if (!ref) return undefined
const value = ref.deref()
if (value === undefined) {
// GC удалил объект — убираем мёртвую запись
this._store.delete(key)
console.log(`[WeakRefCache] GC удалил запись "${key}"`)
}
return value
}
has(key) {
return this.get(key) !== undefined
}
get size() {
return this._store.size // может быть больше живых объектов!
}
}
// Демонстрация концепции
console.log('=== WeakRef концепция ===')
const weakCache = new WeakRefCache()
let userData = { id: 1, name: 'Алиса Петрова', role: 'admin' }
weakCache.set('user:1', userData)
console.log('После set — объект жив:', weakCache.get('user:1')?.name) // 'Алиса Петрова'
// В реальном коде: userData = null → GC может удалить объект
// После этого weakCache.get('user:1') вернёт undefined
console.log('Пока userData существует:', weakCache.has('user:1')) // true
// ===
// Практичная альтернатива: кэш с TTL (Time-To-Live)
// Поскольку GC недетерминирован, в реальных приложениях чаще используют TTL-кэш
class TTLCache {
constructor(defaultTtlMs = 60000) {
this._store = new Map()
this._defaultTtl = defaultTtlMs
}
set(key, value, ttlMs = this._defaultTtl) {
this._store.set(key, {
value,
expiresAt: Date.now() + ttlMs,
})
}
get(key) {
const entry = this._store.get(key)
if (!entry) return undefined
if (Date.now() > entry.expiresAt) {
this._store.delete(key)
return undefined // запись истекла
}
return entry.value
}
has(key) {
return this.get(key) !== undefined
}
// Удалить все истёкшие записи (можно вызывать периодически)
cleanup() {
const now = Date.now()
let removed = 0
for (const [key, entry] of this._store) {
if (now > entry.expiresAt) {
this._store.delete(key)
removed++
}
}
return removed
}
get size() { return this._store.size }
}
console.log('\n=== TTL Cache демонстрация ===')
const cache = new TTLCache(500) // TTL 500ms для демонстрации
// Кэшируем результаты вычислений
function expensiveCalc(n) {
const cacheKey = `calc:${n}`
const cached = cache.get(cacheKey)
if (cached !== undefined) {
console.log(`[${cacheKey}] из кэша: ${cached}`)
return cached
}
// "Дорогое" вычисление
const result = Array.from({ length: n }, (_, i) => i + 1).reduce((a, b) => a + b, 0)
cache.set(cacheKey, result)
console.log(`[${cacheKey}] вычислено и сохранено: ${result}`)
return result
}
expensiveCalc(100) // вычислено
expensiveCalc(100) // из кэша
expensiveCalc(200) // вычислено
expensiveCalc(200) // из кэша
console.log('Размер кэша:', cache.size) // 2
// Симулируем истечение TTL
console.log('\n=== FinalizationRegistry паттерн (концептуально) ===')
function createFinalizationRegistryDemo() {
const log = []
// В реальном коде: new FinalizationRegistry(heldValue => { ... })
// Здесь показываем паттерн без реального GC
const registry = {
callbacks: new Map(),
register(target, heldValue) {
log.push(`Зарегистрирован: "${heldValue}"`)
},
// В реальности этот метод вызывается GC автоматически
simulateGC(heldValue) {
log.push(`GC удалил объект → callback для "${heldValue}"`)
}
}
return { registry, log }
}
const { registry, log } = createFinalizationRegistryDemo()
registry.register({}, 'пользователь-42')
registry.register({}, 'изображение-banner.png')
registry.simulateGC('изображение-banner.png')
log.forEach(entry => console.log(entry))Реализуй класс SimpleCache с API как у WeakRef-кэша: методы get(key), set(key, value, ttlMs), has(key) и delete(key). Записи должны истекать через ttlMs миллисекунд. Если ttlMs не передан — запись хранится вечно. Метод get должен автоматически удалять истёкшие записи и возвращать undefined для них.
set: expiresAt = ttlMs !== null ? Date.now() + ttlMs : null. get: if (entry.expiresAt !== null && Date.now() > entry.expiresAt) { this._store.delete(key); return undefined }. has: return this.get(key) !== undefined.