Kurs Vue.js · Moduł 4: Computed i watch
Computed Properties - Automatyczne Obliczenia
W tej lekcji5
Witaj w Centrum Obliczeniowym NOVA LAB! Dyżurny nawigator ma na panelu dwie liczby: prędkość statku i czas lotu. Trzecią, przebyty dystans, przelicza ręcznie po każdej korekcie kursu. Wystarczy jedna zapomniana aktualizacja, a panel pokazuje dystans sprzed godziny i nikt tego nie zauważa. Dr. Nova ma na to prostą zasadę: wartość, którą da się wyliczyć z innych danych, powinna liczyć się sama. W Vue służą do tego właściwości obliczane, czyli computed properties.
Czego się nauczysz
- computed - wartości pochodne, które przeliczają się same i zapamiętują wynik.
- computed z getterem i setterem - pola dwukierunkowe, na przykład przelicznik jednostek energii.
- watch - reagowanie na zmiany: wpis do dziennika, zapytanie do serwera, alarm.
- watchEffect - watcher, który sam wykrywa, z jakich danych korzysta.
- zatrzymywanie watcherów i sprzątanie - zamykanie kanałów telemetrii, gdy przestają być potrzebne.
- praktyka i projekt - dashboard misji łączący wszystkie te narzędzia.
Czym są Computed Properties?
Funkcję computed importujesz z pakietu vue, tak samo jak ref. Przekazujesz jej getter, czyli funkcję strzałkową zwracającą wynik obliczenia. W zamian dostajesz obiekt podobny do ref: w skrypcie jego wynik odczytasz przez .value, a w szablonie po samej nazwie. Poniżej panel nawigacyjny z dwiema danymi wejściowymi w ref i jednym computed, który liczy dystans.
1<template>
2 <div class="navigation-panel" style="background: #0a0e27; color: #00ff88;">
3 <p>Prędkość: {{ velocity }} km/s</p>
4 <p>Czas: {{ timeElapsed }} s</p>
5 <p>Dystans: {{ distanceTraveled }} km</p>
6 </div>
7</template>
8
9<script setup>
10import { ref, computed } from 'vue'
11
12const velocity = ref(25)
13const timeElapsed = ref(3600)
14
15// Computed - automatycznie przelicza dystans
16const distanceTraveled = computed(() => {
17 return velocity.value * timeElapsed.value
18})
19</script>Getter mnoży velocity.value przez timeElapsed.value, a Vue zapamiętuje, które reaktywne wartości przy tym odczytał. To są zależności computed. Gdy zmienisz prędkość z 25 na 30, akapit z dystansem pokaże 108000 bez żadnej dodatkowej linijki kodu. Zwróć uwagę, co się nie zmieniło: velocity i timeElapsed pozostały zwykłymi ref, a computed niczego do nich nie zapisuje, tylko je czyta. Do samego computed też nie przypiszesz wartości - domyślnie jest tylko do odczytu, a próba zapisu kończy się ostrzeżeniem w konsoli. Schemat, który warto mieć w palcach, wygląda zawsze tak: const total = computed(() => a.value + b.value).
Computed vs Methods
Ten sam napis można uzyskać zwykłą funkcją wywołaną w szablonie. Obie drogi dają identyczny tekst na panelu, więc różnica leży gdzie indziej: w tym, kiedy kod się wykonuje. Poniżej wskaźnik paliwa zbudowany na oba sposoby, z console.log w każdym, żeby było widać każde uruchomienie.
1<template>
2 <div class="fuel-display" style="background: #0a0e27; color: #00ff88;">
3 <!-- Computed - wywoływane bez () -->
4 <p>{{ fuelStatus }}</p>
5
6 <!-- Method - wywoływane z () -->
7 <p>{{ getFuelStatus() }}</p>
8 </div>
9</template>
10
11<script setup>
12import { ref, computed } from 'vue'
13
14const fuelLevel = ref(750)
15const maxFuel = ref(1000)
16
17// Computed - cachowane, przeliczane tylko gdy zależności się zmienią
18const fuelStatus = computed(() => {
19 console.log('Computed wykonane')
20 const percentage = (fuelLevel.value / maxFuel.value) * 100
21 return `Paliwo: ${percentage.toFixed(1)}%`
22})
23
24// Method - wykonywana przy każdym renderze
25function getFuelStatus() {
26 console.log('Method wykonane')
27 const percentage = (fuelLevel.value / maxFuel.value) * 100
28 return `Paliwo: ${percentage.toFixed(1)}%`
29}
30</script>W szablonie computed zapisujesz bez nawiasów, bo to wartość, a metodę z nawiasami, bo to wywołanie. Metoda wykonuje się przy każdym renderowaniu komponentu, także wtedy, gdy render wywołała zmiana zupełnie innego pola. Computed uruchamia getter tylko wtedy, gdy zmieni się fuelLevel albo maxFuel, a w pozostałych renderach oddaje zapamiętany wynik. Obalmy przy okazji trzy mity. Computed nie jest "zawsze szybszy" - cały zysk bierze się z zapamiętania wyniku. Computed nie przyjmuje argumentów - gdy potrzebujesz parametru, sięgnij po metodę. Computed nie działa też asynchronicznie: getter musi od razu zwrócić wartość, więc await i zapytania do serwera nie mają w nim miejsca.
Cachowanie Computed
Zapamiętany wynik to cache. Najbardziej opłaca się przy kosztownych obliczeniach, na przykład przy uśrednianiu setek odczytów z czujników. W przykładzie metoda tablicowa reduce przechodzi po odczytach i sumuje je w jedną liczbę, a sumę dzielimy potem przez liczbę odczytów.
1<script setup>
2import { ref, computed } from 'vue'
3
4const sensorReadings = ref([25, 30, 28, 32, 29])
5
6// Kosztowna operacja - ale cachowana!
7const averageTemperature = computed(() => {
8 console.log('Obliczanie średniej temperatury...')
9 return sensorReadings.value.reduce((sum, reading) => {
10 // Symulacja kosztownej operacji
11 return sum + reading
12 }, 0) / sensorReadings.value.length
13})
14
15// Wiele odwołań - tylko jedno obliczenie
16// {{ averageTemperature }}
17// {{ averageTemperature }}
18// {{ averageTemperature }}
19</script>Trzy komentarze z {{ averageTemperature }} udają trzy miejsca w szablonie, które pokazują średnią. Mimo trzech odwołań komunikat o obliczaniu średniej pojawi się w konsoli raz. A co się stanie, gdy do sensorReadings dojdzie nowy odczyt? Vue nie przelicza średniej od razu. Najpierw oznacza computed jako nieaktualny - w kodzie źródłowym Vue ta flaga nazywa się "dirty". Getter uruchomi się dopiero przy najbliższym odczycie wartości, a nowy wynik trafi do cache i zostanie zwrócony. Jeśli nikt średniej nie odczyta, getter w ogóle się nie wykona. Pamiętaj też, że zależnością może być tylko dana reaktywna: computed oparty na Date.now() nigdy by się nie odświeżył.
Filtrowanie listy modułów
Typowe zadanie dla computed to lista przefiltrowana według tego, co operator wpisał w wyszukiwarkę. Metoda filter zostawia w tablicy tylko elementy spełniające warunek, toLowerCase zamienia tekst na małe litery, a includes sprawdza, czy nazwa zawiera szukany fragment.
1import { ref, computed } from 'vue'
2
3const searchQuery = ref('')
4const modules = ref([
5 { id: 1, name: 'Life Support System' },
6 { id: 2, name: 'Oxygen Recycler' }
7])
8
9const filteredModules = computed(() => {
10 const query = searchQuery.value.toLowerCase()
11 return modules.value.filter((mod) => mod.name.toLowerCase().includes(query))
12})Ten computed ma dwie zależności, searchQuery i modules, więc lista odświeży się zarówno po wpisaniu litery, jak i po dodaniu nowego modułu. Dzięki zamianie obu stron na małe litery wielkość liter przestaje mieć znaczenie. Oryginalna tablica się nie zmienia, bo filter zawsze zwraca nową, a puste pole wyszukiwania przepuszcza wszystkie moduły, ponieważ każda nazwa zawiera pusty fragment.
Polecam prostą zasadę: wszystko, co da się wyliczyć z innych danych, trzymaj w computed, a nie w osobnym ref aktualizowanym ręcznie. Jedno źródło prawdy to mniej miejsc, w których można zapomnieć o aktualizacji. W następnej lekcji nauczysz computed przyjmować zapis, a potem poznasz watch - narzędzie na chwile, gdy zamiast wartości potrzebujesz akcji.
Pamiętaj: computed to komputer nawigacyjny stacji - przelicza kurs tylko wtedy, gdy zmieni się któryś z parametrów, a między zmianami podaje zapamiętany wynik.
Kod do tej lekcji: App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// NOVA LAB Control Center - Mission Statistics
5const missionDays = ref(127)
6const crewMembers = ref(8)
7const oxygenLevel = ref(87)
8const powerOutput = ref(2400)
9const waterReserves = ref(3200)
10
11// Computed properties - automatically recalculated
12const missionWeeks = computed(() => {
13 return Math.floor(missionDays.value / 7)
14})
15
16const oxygenPerPerson = computed(() => {
17 return (oxygenLevel.value / crewMembers.value).toFixed(1)
18})
19
20const powerPerPerson = computed(() => {
21 return Math.floor(powerOutput.value / crewMembers.value)
22})
23
24const systemStatus = computed(() => {
25 if (oxygenLevel.value > 80 && powerOutput.value > 2000) {
26 return 'OPTIMAL'
27 } else if (oxygenLevel.value > 60 && powerOutput.value > 1500) {
28 return 'CAUTION'
29 }
30 return 'CRITICAL'
31})
32
33const statusColor = computed(() => {
34 const colors = {
35 'OPTIMAL': '#00ff88',
36 'CAUTION': '#ffaa00',
37 'CRITICAL': '#ff0044'
38 }
39 return colors[systemStatus.value]
40})
41
42function adjustOxygen(delta) {
43 oxygenLevel.value = Math.max(0, Math.min(100, oxygenLevel.value + delta))
44}
45
46function adjustPower(delta) {
47 powerOutput.value = Math.max(0, powerOutput.value + delta)
48}
49</script>
50
51<template>
52 <div class="control-center">
53 <div class="header">
54 <h1>NOVA LAB Control Center</h1>
55 <div class="mission-time">Sol {{ missionDays }} (Week {{ missionWeeks }})</div>
56 </div>
57
58 <div class="status-panel" :style="{ borderColor: statusColor }">
59 <h2>System Status: <span :style="{ color: statusColor }">{{ systemStatus }}</span></h2>
60 <div class="crew-info">Crew: {{ crewMembers }} members</div>
61 </div>
62
63 <div class="stats-grid">
64 <div class="stat-card">
65 <div class="stat-label">Oxygen Level</div>
66 <div class="stat-value">{{ oxygenLevel }}%</div>
67 <div class="stat-computed">{{ oxygenPerPerson }}% per person</div>
68 <div class="controls">
69 <button @click="adjustOxygen(-5)">-5%</button>
70 <button @click="adjustOxygen(5)">+5%</button>
71 </div>
72 </div>
73
74 <div class="stat-card">
75 <div class="stat-label">Power Output</div>
76 <div class="stat-value">{{ powerOutput }}W</div>
77 <div class="stat-computed">{{ powerPerPerson }}W per person</div>
78 <div class="controls">
79 <button @click="adjustPower(-100)">-100W</button>
80 <button @click="adjustPower(100)">+100W</button>
81 </div>
82 </div>
83
84 <div class="stat-card">
85 <div class="stat-label">Water Reserves</div>
86 <div class="stat-value">{{ waterReserves }}L</div>
87 <div class="stat-computed">{{ (waterReserves / crewMembers).toFixed(1) }}L per person</div>
88 </div>
89 </div>
90
91 <div class="info-box">
92 <strong>Computed Properties:</strong> Values automatically update when dependencies change!
93 </div>
94 </div>
95</template>
96
97<style>
98* {
99 margin: 0;
100 padding: 0;
101 box-sizing: border-box;
102}
103
104body {
105 font-family: 'Courier New', monospace;
106 background: #0a0e27;
107 color: #00ff88;
108 padding: 20px;
109}
110
111.control-center {
112 max-width: 900px;
113 margin: 0 auto;
114}
115
116.header {
117 text-align: center;
118 margin-bottom: 30px;
119 padding: 20px;
120 background: linear-gradient(135deg, #1a1f4c 0%, #0a0e27 100%);
121 border: 2px solid #00ff88;
122 border-radius: 10px;
123}
124
125.header h1 {
126 font-size: 2rem;
127 margin-bottom: 10px;
128 text-shadow: 0 0 20px #00ff88;
129}
130
131.mission-time {
132 color: #00b4d8;
133 font-size: 1.2rem;
134}
135
136.status-panel {
137 padding: 20px;
138 margin-bottom: 30px;
139 background: rgba(0, 255, 136, 0.05);
140 border: 2px solid;
141 border-radius: 10px;
142 text-align: center;
143 transition: all 0.3s ease;
144}
145
146.status-panel h2 {
147 font-size: 1.5rem;
148 margin-bottom: 10px;
149}
150
151.crew-info {
152 color: #00b4d8;
153 font-size: 1.1rem;
154}
155
156.stats-grid {
157 display: grid;
158 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
159 gap: 20px;
160 margin-bottom: 30px;
161}
162
163.stat-card {
164 background: rgba(0, 180, 216, 0.05);
165 border: 2px solid #00b4d8;
166 border-radius: 10px;
167 padding: 20px;
168 text-align: center;
169}
170
171.stat-label {
172 color: #00b4d8;
173 font-size: 0.9rem;
174 margin-bottom: 10px;
175 text-transform: uppercase;
176}
177
178.stat-value {
179 font-size: 2.5rem;
180 font-weight: bold;
181 color: #00ff88;
182 margin-bottom: 10px;
183 text-shadow: 0 0 10px #00ff88;
184}
185
186.stat-computed {
187 color: #ffaa00;
188 font-size: 1rem;
189 margin-bottom: 15px;
190 font-style: italic;
191}
192
193.controls {
194 display: flex;
195 gap: 10px;
196 justify-content: center;
197}
198
199button {
200 background: linear-gradient(135deg, #00b4d8 0%, #0077aa 100%);
201 color: white;
202 border: none;
203 padding: 10px 20px;
204 border-radius: 5px;
205 cursor: pointer;
206 font-family: 'Courier New', monospace;
207 font-weight: bold;
208 transition: all 0.3s ease;
209}
210
211button:hover {
212 background: linear-gradient(135deg, #00ff88 0%, #00cc66 100%);
213 transform: translateY(-2px);
214 box-shadow: 0 5px 15px rgba(0, 255, 136, 0.3);
215}
216
217button:active {
218 transform: translateY(0);
219}
220
221.info-box {
222 background: rgba(0, 255, 136, 0.1);
223 border: 2px solid #00ff88;
224 border-radius: 10px;
225 padding: 15px;
226 text-align: center;
227 color: #00ff88;
228}
229</style>Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Czym jest computed property we Vue.js?
2. Jaka jest główna zaleta computed properties nad zwykłymi metodami?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Uzupełnij kod - stwórz computed property obliczające dystans na podstawie prędkości i czasu.
- Klikanie w kolejności
Ułóż poprawną składnię tworzenia computed property:
- Edytor kodu
Uzupełnij computed property filtrujące listę modułów na podstawie wpisanego zapytania.
- Układanie w pionie
Ułóż etapy działania computed property od zmiany danych do wyniku: