← JavaScript/WeakRef и FinalizationRegistry#143 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

WeakRef и FinalizationRegistry

Представь редактор изображений в браузере. Пользователь открыл 200 фотографий, каждая по 5 МБ. Кэшировать декодированные данные удобно — но если держать все 200 в памяти, браузер упадёт. Нужен кэш, который сам освобождает память когда она нужна системе. Это и есть задача WeakRef.

Что решает этот механизм

Обычный Map держит объекты в памяти вечно — GC не может их удалить. WeakRef позволяет создать ссылку на объект, которая не мешает GC. Объект живёт пока кто-то другой держит сильную ссылку — но как только они кончаются, GC вправе его удалить.

На основе предыдущих уроков

  • объекты, ссылки — разница между значением и ссылкой, основа понимания GC
  • Map, Set — WeakRef часто используется вместе с Map для кэша
  • WeakMap, WeakSet — WeakRef дополняет эти структуры для более гибких сценариев
  • Сильные и слабые ссылки

    Обычная ссылка на объект называется сильной — пока она существует, 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: основное применение — кэш

    Главная задача 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
      }
    }

    FinalizationRegistry

    Позволяет зарегистрировать 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

    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 vs WeakMap vs WeakSet

    | | 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) }
      }
    }

    В реальных проектах

  • Кэш декодированных изображений: браузерные редакторы (Figma, Canva) используют слабые ссылки чтобы не держать все растровые данные в памяти
  • Мемоизация с автоочисткой: библиотека @vue/reactivity внутри использует WeakRef для отслеживания зависимостей компонентов
  • Профилирование и DevTools: инструменты отладки используют 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))

    WeakRef и FinalizationRegistry

    Представь редактор изображений в браузере. Пользователь открыл 200 фотографий, каждая по 5 МБ. Кэшировать декодированные данные удобно — но если держать все 200 в памяти, браузер упадёт. Нужен кэш, который сам освобождает память когда она нужна системе. Это и есть задача WeakRef.

    Что решает этот механизм

    Обычный Map держит объекты в памяти вечно — GC не может их удалить. WeakRef позволяет создать ссылку на объект, которая не мешает GC. Объект живёт пока кто-то другой держит сильную ссылку — но как только они кончаются, GC вправе его удалить.

    На основе предыдущих уроков

  • объекты, ссылки — разница между значением и ссылкой, основа понимания GC
  • Map, Set — WeakRef часто используется вместе с Map для кэша
  • WeakMap, WeakSet — WeakRef дополняет эти структуры для более гибких сценариев
  • Сильные и слабые ссылки

    Обычная ссылка на объект называется сильной — пока она существует, 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: основное применение — кэш

    Главная задача 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
      }
    }

    FinalizationRegistry

    Позволяет зарегистрировать 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

    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 vs WeakMap vs WeakSet

    | | 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) }
      }
    }

    В реальных проектах

  • Кэш декодированных изображений: браузерные редакторы (Figma, Canva) используют слабые ссылки чтобы не держать все растровые данные в памяти
  • Мемоизация с автоочисткой: библиотека @vue/reactivity внутри использует WeakRef для отслеживания зависимостей компонентов
  • Профилирование и DevTools: инструменты отладки используют 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.

    Загружаем среду выполнения...
    Загружаем AI-помощника...