Skip to main content

Що робить WeakRef

WeakRef (weak reference) це об'єкт, який містить слабке посилання на інший об'єкт, тобто посилання, що не заважає збирачу сміття видалити цей об'єкт. Простіше кажучи, WeakRef дозволяє «пам'ятати» об'єкт, поки він живий, але не утримувати його в пам'яті штучно.

Теорія

TL;DR

  • new WeakRef(obj) створює слабке посилання на obj.
  • weakRef.deref() повертає об'єкт або undefined, якщо GC його вже зібрав.
  • Слабке посилання не заважає збиранню сміття, на відміну від звичайного.
  • Момент запуску GC непередбачуваний, тому час життя об'єкта гарантувати не можна.
  • Корисний у вузьких сценаріях: кеш без витоків, мемоізація, спостерігачі, важкі ресурси.
  • FinalizationRegistry дає callback, коли об'єкт зібрано, але без гарантій за часом.
  • Доданий у ES2021; у звичайному коді потреби в ньому зазвичай немає.

Швидкий приклад

javascript
let user = { name: 'Tim' }; const weakRef = new WeakRef(user); user = null; // there are no strong references any more // ...later, maybe... console.log(weakRef.deref()?.name); // 'Tim' or undefined

Тепер об'єкт може бути видалений GC будь-якої миті, і тоді weakRef.deref() поверне undefined.

Сильні та слабкі посилання

Звичайне (сильне) посилання утримує об'єкт у пам'яті:

javascript
let user = { name: 'Tim' }; let ref = user; // an ordinary, strong reference user = null; // but ref still holds the object console.log(ref.name); // 'Tim'

Об'єкт не буде видалений збирачем сміття, поки існує хоча б одне сильне посилання (ref).

Слабке посилання поводиться інакше: воно не рахується при вирішенні того, чи об'єкт ще потрібен.

Як працює WeakRef

  1. new WeakRef(obj) створює слабке посилання.
  2. weakRef.deref() намагається повернути оригінальний об'єкт, якщо він ще живий.
  3. Якщо об'єкт уже зібрано GC, deref() поверне undefined.

Реальна поведінка виглядає так:

javascript
let ref; (() => { let bigObject = { data: 'Some heavy data...' }; ref = new WeakRef(bigObject); })(); // bigObject is no longer reachable directly setTimeout(() => { console.log(ref.deref()); // either the object or undefined, up to the GC }, 1000);

Збирач сміття запускається у невизначений момент. Передбачити, коли саме deref() почне повертати undefined, неможливо.

Навіщо потрібен WeakRef

Зазвичай у JavaScript не треба дбати про ручне керування пам'яттю, але є вузькі випадки, де WeakRef корисний:

СценарійПриклад
Кешування без витоківХочемо кешувати об'єкти, але не тримати їх у пам'яті «вічно»
МемоізаціяЗберігати результат обчислення для об'єкта, поки той живий
Реактивні системи та спостерігачіСтежити за об'єктом, але не заважати його збиранню
Ігрові рушії та графікаКешувати важкі ресурси (текстури, дані), але дозволяти GC їх очищати
Інтеграція з FinalizationRegistryРеагувати, коли об'єкт зібрано GC

Звичайний Map утримує ключі сильно:

javascript
const cache = new Map(); function getUserData(user) { if (!cache.has(user)) { cache.set(user, heavyCompute(user)); } return cache.get(user); }

Проблема в тому, що навіть коли user більше не потрібен, Map не дасть GC його видалити, доки кеш не очистять вручну. Рішення, зберігати у кеші слабке посилання:

javascript
const cache = new Map(); function getUserData(user) { const ref = cache.get(user); let data = ref?.deref(); if (!data) { data = heavyCompute(user); cache.set(user, new WeakRef(data)); } return data; }

Тепер, якщо data більше ніде не використовується, GC її звільнить, і ref.deref() просто поверне undefined.

Зв'язка з FinalizationRegistry

Щоб дізнатися, коли об'єкт було зібрано GC, використовують FinalizationRegistry:

javascript
const registry = new FinalizationRegistry(id => { console.log(`Object ${id} was removed from memory`); }); let user = { name: 'Tim' }; registry.register(user, 'user#1'); let ref = new WeakRef(user); user = null;

Коли GC збере user, викличеться callback:

text
Object user#1 was removed from memory

Важливо: виклик не гарантований за часом і може взагалі не статися до завершення процесу.

Коли не варто використовувати WeakRef

Не використовуйте його:

  • для керування звичайними даними;
  • щоб «пришвидшити» GC, це марно;
  • для зберігання стану в React, Vue чи Next.js;
  • у критичних логічних ланцюжках, де результат не може раптово зникнути.

Що треба пам'ятати:

ОсобливістьОпис
WeakRef не запобігає збираннюОб'єкт може зникнути будь-якої миті
deref() може повернути undefinedЯкщо GC уже видалив об'єкт
Не можна покладатися на час життяGC непередбачуваний
Не використовувати без потребиМоже призвести до «зомбі»-помилок, коли об'єкт зник раптово
Корисний лише у спеціальних сценаріяхКеш, мемоізація, інтеграція з GC

Підсумок:

ПитанняВідповідь
Що робитьСтворює слабке посилання на об'єкт
Як працюєНе заважає GC видалити об'єкт
Метод.deref() повертає об'єкт або undefined
Корисний дляКешів, мемоізації, відстеження пам'яті
РизикиОб'єкт може зникнути будь-якої миті
З'явився вES2021

Типові помилки

  • Очікують, що deref() завжди поверне об'єкт. Він може повернути undefined; завжди перевіряйте результат.
  • Кешують результат deref() у довгоживучій змінній. Це створює сильне посилання і зводить нанівець сенс WeakRef.
  • Плутають WeakRef з WeakMap. WeakMap робить слабкими саме ключі і не дає їх перебирати, WeakRef це одне посилання на одне значення.
  • Розраховують на FinalizationRegistry як на деструктор. Callback може не спрацювати взагалі, тому звільняти файли чи сокети через нього не можна.
  • Використовують WeakRef, щоб «допомогти» збирачу сміття. GC працює за власним розкладом, слабке посилання його не пришвидшує.
  • Тестують поведінку без урахування непередбачуваності GC. Тест, який чекає undefined після setTimeout, буде нестабільним.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.