Czym właściwie jest reaktywność?
Zespół Vue3 tłumaczy pojęcie reaktywności, posługując się prostą analogią do arkusza kalkulacyjnego. To świetny przykład, ponieważ pozwala natychmiast zrozumieć sedno mechanizmu, bez zbędnego komplikowania teorii.
Wyobraź sobie komórki w Excelu. Komórki A0 i A1 są statyczne – służą jedynie do przechowywania wpisanych wartości. Z kolei komórka A2 jest dynamiczna; zawiera funkcję, która sumuje dane z dwóch poprzednich pól. Reaktywność objawia się właśnie tutaj: każda zmiana w A0 lub A1 wywołuje natychmiastową reakcję i aktualizację wyniku w komórce A2.

Pomocniczy przykład w Excel
W świecie tworzenia interfejsów użytkownika (UI) reaktywność polega na odzwierciedleniu stanu (danych, które mamy w JavaScript) na widoku (tym, co widzi użytkownik w przeglądarce).
System reaktywności odpowiada za to, że gdy stan się zmienia, widok aktualizuje się automatycznie, bez dodatkowej ingerencji z naszej strony (ingerencji w DOM). Być może wiesz lub nie, ale w tzw. czystym JavaScript, aby odzwierciedlić stan na widoku (np. zmiana tekstu na przycisku), należałoby najpierw pobrać element, w którym ów tekst miałby zostać zmieniony, za pomocą metody np. getElementById("#button") i następnie przypisać właściwości textContent, nową wartość. A to tylko prosty przykład, w rzeczywistości trzeba byłoby napisać dużo więcej kodu, który dodatkowo byłby bardziej podatny na błędy i trudny w debugowaniu.
Na szczęście w dzisiejszych z pomocą przychodzą frameworki takie jak omawiany tutaj Vue, który pozwala nam zająć się implementacją logiki i widoku w sposób bardziej deklaratywny. Uważam natomiast, że wiedza o tym, jak działa reaktywność pod maską pozwala na dużo lepsze zrozumienie nie tylko Vue, ale programowania ogólnie. Po tym nieco przydługawym wstępie, zapraszam do lektury 🙂
Proxy
Skoro wiesz już, że Vue automatycznie odświeża widok, gdy zmienią się dane, możesz się zastanawiać: skąd Vue w ogóle wie, że taka zmiana miała miejsce? 🤔
W starszym wydaniu Vue (wersja 2) działo się to w sposób dość ograniczony. Nie rozpisując się szczegółowo na temat poprzedniej wersji, warto wiedzieć, że Vue 2 opierał się na Object.defineProperty. System ten miał jednak swoje wady, główną był brak reaktywności przy dodawaniu nowych właściwości do obiektu. Można to było rozwiązać ręcznie za pomocą Vue.set(), ale było to mało intuicyjne.
Dlaczego Vue 2 korzystało z tego rozwiązania? Odpowiedź jest prosta: kompatybilność. W tamtym czasie standardem było wspieranie przeglądarki Internet Explorer 11, która nie potrafiła obsłużyć nowocześniejszego mechanizmu Proxy.
Vue 3 wprowadziło nową jakość, wykorzystując ten mechanizm. Stało się to możliwe dopiero po porzuceniu wsparcia dla IE11. Proxy nie potrzebuje przerabiać każdej właściwości obiektu z osobna ani nie interesuje się tym, jakie klucze masz w obiekcie już na starcie.
Możesz sobie wyobrazić, że Proxy stoi jakby przed obiektem i przechwytuje każdą próbę kontaktu z nim. Jeśli dodasz właściwość, której wcześniej w nim nie było, Proxy natychmiast to wyłapie. Choć pod spodem Vue 3 nadal korzysta z getterów i setterów, działają one teraz jako tzw. "pułapki" (traps) wewnątrz Proxy. To właśnie one są sercem reaktywności i dają Vue sygnał, że czas odświeżyć stronę.
Zachęcam także do lektury osobnego artykułu na temat Proxy, co pozwoli na zgłębienie tematu jeszcze bardziej.
Jak zadeklarować reaktywność?
Skoro wiesz już, że Vue potrzebuje czegoś w rodzaju strażnika (Proxy lub getterów/setterów), aby śledzić zmiany, musisz dowiedzieć się, jak w praktyce wskazać mu dane, które mają zostać objęte taką ochroną. W Vue 3 mamy do dyspozycji dwa główne narzędzia: ref oraz reactive. Choć oba służą do tego samego (tworzenia reaktywnych danych), różnią się sposobem działania i przeznaczeniem.
Ref
Zanim przejdziemy do składni Vue, warto przyjrzeć się temu, jak JavaScript standardowo obsługuje zmienne. Spójrz na poniższy przykład:
W powyższym kodzie zmiana count nie ma żadnego wpływu na observedCount. Dzieje się tak, ponieważ typy proste (np. string, number, boolean) są w JavaScript przekazywane przez wartość, czyli kopiowane. Vue nie ma możliwości podpięcia się pod taką zmienną, aby śledzić jej zmiany.
Rozwiązaniem tego problemu jest referencja. W JavaScript obiekty są przekazywane przez referencję (adres w pamięci), co oznacza, że wiele zmiennych może wskazywać na ten sam obiekt.
Jeśli chcesz głębiej poznać temat zmiennych w JavaScript, kiedyś pisałem o tym osobny artykuł.
Tutaj zmiana w myData jest widoczna w observedData, ponieważ obie zmienne patrzą na to samo miejsce w pamięci. I to jest dokładnie to, co robi ref.
ref pobiera Twoją wartość i zamyka ją wewnątrz obiektu pod kluczem value. Dzięki temu Vue może utrzymać stałą referencję do obiektu, nawet gdy zawartość w środku się zmienia. Może też zastosować gettery i settery na właściwości value, aby wiedzieć, kiedy ją odczytujesz, a kiedy nadpisujesz.
Technicznie warto zapamiętać, że jeśli do ref przekażesz typ prosty, Vue używa wspomnianych getterów i setterów. Jeśli jednak do ref przekażesz typ złożony (czyli obiekt, tablicę czy kolekcje typu Map i Set ), Vue automatycznie użyje pod spodem mechanizmu reactive (czyli Proxy), o którym powiem za chwilę.
Największą zaletą ref jest jego stabilność. Możesz podmienić całą zawartość value na zupełnie nowy obiekt (np. dane pobrane z API), a system reaktywności nadal będzie działał, bo sama referencja do zmiennej ref pozostaje nienaruszona.
Reactive
reactive to drugie narzędzie do tworzenia stanu, ale o znacznie węższym zastosowaniu. W przeciwieństwie do uniwersalnego ref, służy ono wyłącznie do obsługi wspomnianych wcześniej typów złożonych. Nie tworzy ono dodatkowego opakowania value, lecz używa mechanizmu Proxy bezpośrednio na Twoim obiekcie.
Dzięki temu praca z danymi wygląda bardziej naturalnie, niemal jak w czystym JavaScript:
Brak konieczności dopisywania .value wydaje się kuszące, jednak reactive wiąże się z dwoma poważnymi ograniczeniami, o których musisz pamiętać:
problem z ponownym przypisaniem - reaktywność w
reactivejest nierozerwalnie związana z konkretną instancją obiektu Proxy, którą tworzy Vue. W momencie, gdy do zmiennej zadeklarowanej jakoreactiveprzypiszesz całkowicie nowy obiekt, następuje nadpisanie adresu w pamięci, pod który kieruje ta zmienna. W rezultacie wskaźnik przestaje odwoływać się do instancji Proxy, a zaczyna do zwykłego obiektu, co skutkuje natychmiastowym przerwaniem śledzenia zmian przez framework.
utrata reaktywności przy destrukturyzacji - jeśli spróbujesz rozbić obiekt
reactiveza pomocą destrukturyzacji, wyciągnięte właściwości stają się zwykłymi zmiennymi i tracą połączenie z systemem reaktywności Vue.
Jeśli temat destrukturyzacji jest Tobie obcy, zapraszam do zapoznania się z tematem w osobnym artykule.
Podsumowując: reactive oferuje czystszą składnię, ale wymaga od programisty dyscypliny w zarządzaniu referencjami. Właśnie dlatego w większości przypadków bezpieczniejszym i bardziej przewidywalnym wyborem jest ref.
ToRefs
Możesz pomyśleć, że skoro ref jest bezpieczniejszy, to problem destrukturyzacji go nie dotyczy. Niestety, jeśli Twój ref przechowuje obiekt, wpadniesz w tę samą pułapkę, co przy reactive.
Pamiętaj, że gdy do ref przekazujesz obiekt, jego właściwość value staje się obiektem Proxy. Używając na nim destrukturyzacji, tworzysz po prostu nowe, niezależne zmienne i przypisujesz do nich aktualne wartości pól obiektu. Przez to tracą one powiązanie z pierwotnym obiektem i przestają być śledzone przez system reaktywności Vue.
Czy zatem niemożliwym jest zastosowanie destrukturyzacji? Nic bardziej mylnego. Vue oferuje do tego funkcję toRefs. Zamienia ona każdą właściwość obiektu na osobny, niezależny ref. Dzięki temu zachowujemy połączenie z oryginałem, a jednocześnie możemy cieszyć się wygodniejszą strukturą danych.
Co istotne, toRefs zadziała identycznie zarówno na obiekcie zadeklarowanym jako reactive, jak i na obiekcie ukrytym wewnątrz ref - w obu przypadkach skutecznie zabezpieczy reaktywność poszczególnych pól.
Mechanizm powiązań
Wiedząc już, że Proxy przechwytuje moment odczytu i zapisu danych, musimy zrozumieć, co Vue robi z tą informacją. Sam fakt, że zmienna uległa zmianie, nie wystarczy - framework musi wiedzieć, które konkretne fragmenty aplikacji (np. funkcje renderujące komponenty) powinny zostać wywołane ponownie.
Proces ten opiera się na dwóch operacjach, które zarządzają globalną strukturą zależności:
Śledzenie (track)
W momencie, gdy dowolny kod (np. szablon komponentu) odczytuje wartość zmiennej reaktywnej, uruchamiany jest jej getter. Vue wykonuje wtedy operację track.
Kluczowym elementem jest tutaj globalna mapa zależności. Jest to struktura, w której Vue przechowuje powiązania między obiektami, a funkcjami, które ich używają. Jeśli odczyt następuje podczas renderowania komponentu, Vue zapisuje ten komponent (jako tzw. effect) na liście subskrybentów danej właściwości.
Wyzwalanie (trigger)
Gdy przypisujesz nową wartość do zmiennej, uruchamiany jest setter. Vue wykonuje wtedy operację trigger.
Framework zagląda do swojej globalnej mapy, odszukuje właściwość, która właśnie została zmieniona, i pobiera listę wszystkich przypisanych do niej efektów. Następnie wszystkie te funkcje (np. ponowne renderowanie komponentu) są natychmiast uruchamiane, co skutkuje aktualizacją widoku w przeglądarce.
Dzięki temu, że Vue operuje na precyzyjnej mapie powiązań, system jest niezwykle dokładny. Framework nie musi zgadywać, co się zmieniło, ani sprawdzać całego drzewa komponentów po każdej drobnej zmianie. Dokładnie wie, które funkcje w pamięci wymagają ponownego wykonania, ponieważ zostały one zarejestrowane w globalnej strukturze w momencie pierwszego odczytu danych.
Debugowanie reaktywności
Gdy system powiązań nie działa tak, jak oczekujesz, Vue 3 pozwala podpiąć się pod operacje śledzenia i wyzwalania. Możesz to zrobić na dwa sposoby, w zależności od tego, czy chcesz monitorować cały komponent, czy tylko konkretną logikę.
Debugowanie całego komponentu
Jeśli chcesz monitorować warstwę prezentacji i zrozumieć, co wymusza ponowne renderowanie (re-render) widoku, użyj hooków cyklu życia. Importujemy je bezpośrednio z Vue.
Debugowanie pojedynczego obserwatora
Często nie interesuje nas cały komponent, a jedynie konkretny proces w tle. Wtedy warto użyć opcji debugujących wewnątrz watchEffect (lub computed). Pozwala to odizolować logikę biznesową od reszty aplikacji.
W funkcjach takich jak watchEffect, Vue pozwala na przekazanie drugiego argumentu z opcjami debugującymi. Jest to najprecyzyjniejsza metoda śledzenia zależności konkretnej operacji logicznej, całkowicie odizolowana od renderowania widoku.
Zauważ, że w przypadku ref, klucz zdarzenia (e.key) zawsze przyjmie wartość value. To praktyczne potwierdzenie struktury ref, o której wspominałem wcześniej - Vue śledzi zmiany we właściwości opakowującej, a nie bezpośrednio w samej liczbie.
Podsumowanie
Zrozumienie technicznych fundamentów reaktywności Vue 3 pozwala uniknąć najczęstszych pułapek związanych z utratą śledzenia zmian, szczególnie podczas destrukturyzacji czy nadpisywania obiektów. W codziennej pracy najbezpieczniejszą strategią jest wybór ref, jako domyślnego narzędzia do zarządzania stanem. Dzięki stałej referencji do właściwości value oraz uniwersalnej obsłudze wszystkich typów danych, ref zapewnia największą stabilność i przewidywalność kodu w niemal każdym scenariuszu projektowym.
