Використання Proxy та Reflect для створення реактивних об’єктів у JavaScript

Реактивність є основою багатьох сучасних фреймворків, таких як Vue.js, Svelte та навіть внутрішніх механізмів React. Вона дозволяє автоматично оновлювати UI, коли змінюється стан.

Одним з найефективніших способів створення реактивних об’єктів у JavaScript є Proxy API та Reflect API. У цій статті ми розглянемо, як вони працюють, чому вони краще за Object.defineProperty, та як їх можна використати для створення власного простого реактивного механізму.

Що таке Proxy та Reflect?

Proxy

Об’єкт Proxy дозволяє перехоплювати операції над іншими об’єктами (наприклад, отримання, встановлення, видалення властивостей) та змінювати їхню поведінку.

const obj = { name: "Alice" };

const proxy = new Proxy(obj, {
  get(target, property) {
    console.log(`Отримання значення ${property}`);
    return target[property];
  },
  set(target, property, value) {
    console.log(`Зміна ${property} на ${value}`);
    target[property] = value;
    return true;
  }
});

console.log(proxy.name); // Лог: Отримання значення name
proxy.age = 25; // Лог: Зміна age на 25

Reflect

Reflect – це набір утиліт, який спрощує роботу з об’єктами, даючи змогу безпечно виконувати операції (get, set, deleteProperty тощо).

const obj = { name: "Alice" };

console.log(Reflect.get(obj, "name")); // Alice
Reflect.set(obj, "age", 25);
console.log(obj.age); // 25

Reflect часто використовується разом із Proxy, щоб делегувати стандартну поведінку, не переписуючи її вручну.

Чому Proxy краще за Object.defineProperty()?

До появи Proxy для реактивності використовували Object.defineProperty, але цей підхід має обмеження:

  1. Object.defineProperty працює тільки з окремими властивостями, а Proxy дозволяє працювати з цілим об’єктом.
  2. Неможливо відстежувати додавання/видалення властивостей.
  3. Object.defineProperty не працює з масивами так само ефективно, як Proxy.

Приклад проблеми з Object.defineProperty:

const obj = {};
Object.defineProperty(obj, "name", {
  get() {
    console.log("Отримання значення");
    return "Alice";
  },
  set(value) {
    console.log("Зміна значення");
  }
});

obj.name = "Bob"; // Лог: Зміна значення
console.log(obj.name); // Лог: Отримання значення
delete obj.name; // ❌ Не працює – не можна відстежити видалення властивостей

З Proxy ми можемо легко відстежувати всі зміни.

Створення простого реактивного об’єкта за допомогою Proxy та Reflect

Створимо базовий механізм, який дозволить відстежувати зміни у стані та викликати відповідні функції при оновленні значень.

function reactive(target) {
  const subscribers = new Set(); // Зберігаємо функції, які реагують на зміни

  const notify = () => {
    subscribers.forEach(subscriber => subscriber());
  };

  return new Proxy(target, {
    get(obj, prop) {
      console.log(`Отримання значення ${prop}`);
      return Reflect.get(obj, prop);
    },
    set(obj, prop, value) {
      console.log(`Зміна ${prop} на ${value}`);
      Reflect.set(obj, prop, value);
      notify(); // Викликаємо підписників після зміни
      return true;
    },
    deleteProperty(obj, prop) {
      console.log(`Видалення ${prop}`);
      Reflect.deleteProperty(obj, prop);
      notify();
      return true;
    },
    subscribe(fn) {
      subscribers.add(fn);
    }
  });
}

// Використання
const state = reactive({ count: 0 });

const render = () => console.log(`Рендер: count = ${state.count}`);
state.subscribe(render); // Підписуємо рендер-функцію

state.count = 1; // Лог: Зміна count на 1 → Рендер: count = 1
state.count = 2; // Лог: Зміна count на 2 → Рендер: count = 2
delete state.count; // Лог: Видалення count → Рендер: count = undefined

Реактивні масиви через Proxy

Масиви у JavaScript є об’єктами, тому Proxy може працювати з ними аналогічно.

const reactiveArray = reactive([]);

reactiveArray.subscribe(() => console.log("Масив змінено:", reactiveArray));

reactiveArray.push("React"); // Масив змінено: ["React"]
reactiveArray.push("Vue");   // Масив змінено: ["React", "Vue"]
reactiveArray.pop();         // Масив змінено: ["React"]

Проблема з length у масивах

При видаленні елемента length масиву змінюється, що теж можна перехопити:

const arrayHandler = {
  set(target, prop, value) {
    console.log(`Зміна ${prop} на ${value}`);
    return Reflect.set(target, prop, value);
  }
};

const reactiveNumbers = new Proxy([1, 2, 3], arrayHandler);

reactiveNumbers.push(4); // Лог: Зміна 3 на 4, Зміна length на 4
reactiveNumbers.length = 2; // Лог: Зміна length на 2 (всі зайві елементи видаляться)
console.log(reactiveNumbers); // [1, 2]

Підключення реактивних об’єктів до DOM

Тепер давайте зробимо зв’язок між станом і HTML, щоб створити базову реактивність.

<!DOCTYPE html>
<html lang="uk">
<head>
    <title>Реактивний лічильник</title>
</head>
<body>
    <p id="counter">Лічильник: 0</p>
    <button id="increment">Збільшити</button>

    <script>
        function reactive(target, render) {
            return new Proxy(target, {
                set(obj, prop, value) {
                    obj[prop] = value;
                    render();
                    return true;
                }
            });
        }

        const state = reactive({ count: 0 }, () => {
            document.getElementById("counter").innerText = `Лічильник: ${state.count}`;
        });

        document.getElementById("increment").addEventListener("click", () => {
            state.count++;
        });
    </script>
</body>
</html>

Що ми зробили?

  • Створили реактивний об’єкт state.
  • Підключили його до DOM (текст у <p id="counter"> змінюється при оновленні count).
  • Додали подію click, яка змінює стан.

🚀 Вийшов міні-VueJS без фреймворків!

Висновок

Proxy та Reflect дають змогу створювати потужні механізми реактивності без зайвих складнощів.

🔥 Що ми дізналися:

Proxy дозволяє перехоплювати всі операції з об’єктами.
Reflect допомагає зручно делегувати стандартну поведінку.
✔ Реактивні об’єкти можна використовувати в UI, автоматично оновлюючи DOM.
✔ Масиви також можна зробити реактивними через Proxy.

💡 Proxy – це основа багатьох сучасних фреймворків, і розуміння його механіки допоможе вам глибше зрозуміти Vue, Svelte та навіть внутрішні механізми React. 🚀