Kurs Vue.js · Moduł 4: Computed i watch

Computed Properties - Automatyczne Obliczenia

6 min czytania
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. 1. Czym jest computed property we Vue.js?

  2. 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:

Przydatne artykuły