Використання 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
, але цей підхід має обмеження:
Object.defineProperty
працює тільки з окремими властивостями, а Proxy дозволяє працювати з цілим об’єктом.- Неможливо відстежувати додавання/видалення властивостей.
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. 🚀