Як працює реактивність на JavaScript
Привіт! Якщо ви працюєте з сучасними JavaScript-фреймворками, такими як SolidJS, Vue чи Angular, ви точно стикалися з терміном «реактивність». Це та сама магія, яка автоматично оновлює ваш інтерфейс користувача, коли змінюються дані. Але як вона працює? В SolidJS і Angular є сигнали для цього, у Vue теж є схожий підхід — через ref і reactive, які виконують роль реактивних обгорток для даних.
Давайте розбиратись!
Що таке реактивність
У контексті програмування реактивність — це підхід, де система автоматично реагує на зміни даних. Замість того, щоб вручну оновлювати DOM або викликати функції щоразу, коли щось змінюється, ми створюємо залежності між даними та «ефектами» (наприклад, оновленням UI або виведенням у консоль). Коли дані змінюються, залежні від них ефекти спрацьовують автоматично.
Основні елементи
Щоб реалізувати базову реактивність, нам знадобляться:
- State (стан). Це дані, за якими ми спостерігаємо. Наприклад, рахунок гри або ім’я користувача на сайті. Це значення, які можуть змінюватися.
- Трекінг. Потрібно мати механізм для відстеження. Наприклад, якщо функція оновлює текст на екрані, вона має знати, яке значення (наприклад, ім’я користувача) потрібно показати. Якщо ці дані змінюються, ми повинні оновити відповідну частину інтерфейсу.
- Реакція на зміни. Коли дані змінюються (наприклад, оновлюється лічильник), має спрацювати механізм оповіщення. Він перевіряє, хто підписаний на ці конкретні дані, і надсилає їм повідомлення: «Значення оновлено!».
Реалізація
Оголошення глобальних змінних
Першим кроком нам треба зробити визначення двох змінних, які будуть використовуватись для зберігання поточного ефекту та залежностей:
currentEffect
— тут ми будемо зберігати функцію ефекту, який наразі виконується.dependencies
— а це у нас буде структура даних типуMap
, що буде зберігати залежності, яка саме властивість стану пов’язана з якими функціями (ефектами).
let currentEffect = null; const dependencies = new Map();
Уявіть собі це, як список підписок:
Кожна властивість стану (наприклад, count
або text
) має свій список методів, які потрібно викликати, коли ця властивість змінюється. Ми використовуємо Map
, бо це зручний спосіб зберігати пари «властивість → набір функцій».
Тобто, якщо state.count
зміниться, ми заглядаємо в dependencies
, бачимо, які функції залежать від count
, і запускаємо їх.
Це як система сповіщень: «Змінилася властивість count
— отже, потрібно виконати ось ці функції».
Так ми точно знаємо, що оновлювати, коли змінюється стан.
Функція для відстеження залежностей
Далі нам буде потрібна функція track
яка відповідає за залежності:
- Якщо немає активного ефекту (
currentEffect
), нічого не відбувається. - Якщо є поточний ефект, ми додаємо його до пулу всіх інших ефектів, що залежать від цієї властивості.
function track(property) { if (!currentEffect) return; let effects = dependencies.get(property); if (!effects) { effects = new Set(); dependencies.set(property, effects); } effects.add(currentEffect); }
Тригер змін
Також нам буде потрібен trigger
який запускає усі ефекти:
function trigger(property) { const effects = dependencies.get(property); if (effects) { effects.forEach(effect => effect()); } }
Він отримує всі ефекти та виконує кожен з них.
Навчаємо дані реагувати на зміни
Тепер створимо функцію createState
. Вона бере звичайний об’єкт і робить так, щоб він міг сам реагувати на зміну своїх властивостей. Для цього ми використовуємо Proxy
, який перехоплює читання та запис значень, щоби відстежувати залежності й запускати потрібні ефекти.
- Коли читається, викликається
track
для реєстрації залежностей. - Коли значення змінюється, викликається
trigger
для активації залежних ефектів
function createState(initialState) { return new Proxy(initialState, { get(target, property) { track(property); return Reflect.get(target, property); }, set(target, property, value) { const success = Reflect.set(target, property, value); if (success) { trigger(property); } return success; } }); }
Онови це, коли щось зміниться
Функція createEffect
дозволяє створити реактивність. Тобто, коли певне значення зміниться — ця функція автоматично виконається знову.
- Ми тимчасово зберігаємо функцію, яку передали в
createEffect
, у зміннуcurrentEffect
. Це потрібно, щоб знати, що саме вона читає. - Потім виконуємо її одразу. Під час виконання, якщо вона звертається до якихось значень зі стану (наприклад,
state.count
), система запам’ятовує, що ця функція залежить від цих значень. - Після завершення ми очищаємо
currentEffect
.
function createEffect(effectFn) { currentEffect = effectFn; effectFn(); currentEffect = null; }
Приклад використання:
Створення нашого state:
const state = createState({ count: 0, text: 'Привіт' });
Створюємо два ефекти, які залежать від нашого state:
createEffect(() => { console.log(`Лічильник зараз: ${state.count}`); }); createEffect(() => { console.log(`Текст зараз: ${state.text}`); });
Коли ми змінюємо значення властивостей в нашому state, відповідні ефекти автоматично спрацьовують:
state.count++; // Виведе: "Лічильник зараз: 1" state.text = 'Тест!'; // Виведе: "Текст зараз: Тест!" state.count = 10; // Виведе: "Лічильник зараз: 10"
Погляньмо, як усе працює разом
createState
— перетворює звичайний об’єкт у «реактивний»: коли ми читаємо або змінюємо його властивості.track
— запам’ятовує, яка функція (ефект) використовує які саме дані, щоб потім знати, що потрібно оновити.trigger
— запускає потрібні функції щоразу, коли змінюється якась частина даних.createEffect
— дозволяє нам сказати: «Запусти ось цю функцію, коли зміняться певні значення».
Цей код створює базову реактивність, де зміни в стані автоматично запускають відповідні ефекти, завдяки чому можна реалізувати просту реактивну систему без використання складних бібліотек чи фреймворків.
Чудово, тепер нам треба реалізувати базовову функцію рендерингу, щоб побачити роботу реактивності в дії.
Реалізація компонента
Створимо HTML-обгортку:
<body> <div id="app"></div> </body>
Далі отримуємо DOM-елемент, куди будемо рендерити UI:
const container = document.getElementById('app');
Тепер напишимо наш render()
:
function render() { container.innerHTML = ` <div> <h2>${state.text}</h2> <p>Лічильник: ${state.count}</p> <button data-action="increment">Збільшити</button> <button data-action="changeText">Змінити текст</button> </div> `; }
- Ми вставляємо в нього актуальні значення
state.text
іstate.count
.
Нам треба буде два ефекти для логування змін наших count
та text
, та окремий ефект для render()
:
createEffect(() => { console.log(`Лічильник змінено: count = ${state.count}`); }); createEffect(() => { console.log(`Текст змінено: ${state.text}`); }); createEffect(() => { render() });
render()
малює UI, зчитуючиstate.count
іstate.text
.createEffect(() => render())
запам’ятовує ці залежності.- Якщо змінюється
count
абоtext
— автоматично запускаєтьсяrender()
. - Водночас інші
createEffect
логують зміни конкретних полів.
Встановлення слухачів на наш контейнер:
Замість того, щоб додавати окремий слухач подій для кожної кнопки, ми встановлюємо один слухач на контейнер (елемент з id="app"
).
Це дозволяє обробляти події для всіх дочірніх елементів (в тому числі кнопок), зменшуючи кількість слухачів і покращуючи продуктивність.
const container = document.getElementById('app'); // Встановлюємо делегування подій один раз на контейнер container.addEventListener('click', (event) => { const target = event.target; if (target.tagName === 'BUTTON') { const action = target.dataset.action; if (action === 'increment') { state.count++; } else if (action === 'changeText') { state.text = state.text === 'Привіт 👋' ? 'Як справи?' : 'Привіт 👋'; } } });
При натисканні:
increment
→ збільшує лічильник на 1;changeText
→ перемикає текст між «Привіт 👋» та «Як справи?».
Після виклику CounterComponent()
все починає працювати!
// Компонент function CounterComponent() { const state = createState({ count: 0, text: 'Привіт 👋' }); const container = document.getElementById('app'); function render() { container.innerHTML = ` <div> <h2>${state.text}</h2> <p>Лічильник: ${state.count}</p> <button data-action="increment">Збільшити</button> <button data-action="changeText">Змінити текст</button> </div> `; } createEffect(() => { console.log(`Ререндер: count = ${state.count}`); }); createEffect(() => { console.log(`Текст змінено: ${state.text}`); }); createEffect(() => { render(); }); // Встановлюємо делегування подій один раз на контейнер container.addEventListener('click', (event) => { const target = event.target; if (target.tagName === 'BUTTON') { const action = target.dataset.action; if (action === 'increment') { state.count++; } else if (action === 'changeText') { state.text = state.text === 'Привіт 👋' ? 'Як справи?' : 'Привіт 👋'; } } }); } // Запуск CounterComponent();
createEffect
спрацьовує автоматично, рендерить DOM, коли змінюється стан — компонент перерендерюється, а ми не викликаємо render()
вручну — це робить реактивна система.
Висновок
Ми створили просту реактивну систему на JavaScript без сторонніх бібліотек. Це дозволяє на прикладі зрозуміти базові принципи реактивності, які лежать в основі тогож Vue.js — як відбувається відстеження залежностей, оновлення стану та автоматичний ререндер.
5 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів