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

Computed i Watch w Praktyce - Systemy Misji

7 min czytania
W tej lekcji4

Centrum Kontroli zamówiło trzy panele: wyszukiwarkę modułów, która nie zasypuje serwera zapytaniem po każdej literze, kalkulator trajektorii z historią obliczeń i formularz rejestracji z walidacją. Każdy łączy computed z watcherem, a cała sztuka polega na tym, by nie pomylić ról. Zasada jest krótka: gdy potrzebujesz wartości, pisz computed, a gdy potrzebujesz akcji - zapytania, wpisu do dziennika, timera - pisz watch. Getter computed musi przy tym pozostać czysty: tylko liczy i zwraca wynik, nie wysyła zapytań, nie zapisuje do localStorage i nie zmienia innych danych.

System Wyszukiwania Modułów z Debounce

Operator wpisujący słowo "reaktor" wysłałby siedem zapytań, choć interesuje go tylko ostatnie. Technika debounce wstrzymuje akcję, dopóki dane nie przestaną się zmieniać przez określony czas, tu przez 300 milisekund. Zacznijmy od szablonu z polem wyszukiwania, komunikatem ładowania i listą wyników.

1<template>
2  <div class="module-search-system" style="background: #0a0e27; color: #00ff88;">
3    <input v-model="searchInput" placeholder="Szukaj modułów..." style="background: #1a1e37; color: #00ff88;" />
4    <p v-if="isLoading" style="color: #00b4d8;">Skanowanie bazy NOVA LAB...</p>
5    <ul>
6      <li v-for="item in results" :key="item.id" style="border-bottom: 1px solid #00b4d8;">
7        {{ item.name }}
8      </li>
9    </ul>
10  </div>
11</template>

Pole jest podpięte do searchInput, czyli surowego tekstu, który zmienia się po każdym naciśnięciu klawisza. Lista czyta results, a komunikat pokazuje się, gdy isLoading ma wartość true. Teraz skrypt z pierwszym watcherem, który opóźnia przepisanie tekstu do searchQuery.

1<script setup>
2import { ref, watch } from 'vue'
3
4const searchInput = ref('')
5const searchQuery = ref('')
6const results = ref([])
7const isLoading = ref(false)
8
9// Debounce - opóźnij wyszukiwanie
10let debounceTimeout
11watch(searchInput, (value) => {
12  clearTimeout(debounceTimeout)
13  debounceTimeout = setTimeout(() => {
14    searchQuery.value = value
15  }, 300)
16})

Kolejność kroków warto zapamiętać: zmienna debounceTimeout poza watcherem, potem watch(searchInput, ...), w nim clearTimeout kasujący poprzednie odliczanie i setTimeout ustawiający nowe. Każdy klawisz zeruje zegar, więc do searchQuery trafia dopiero tekst, po którym nastąpiła przerwa. Pole na ekranie nadal pokazuje litery od razu, czeka wyłącznie wyszukiwanie. Drugi watcher wykonuje właściwe zapytanie.

1// Wykonaj wyszukiwanie
2watch(searchQuery, async (query) => {
3  if (!query) {
4    results.value = []
5    return
6  }
7
8  isLoading.value = true
9
10  try {
11    const response = await fetch(`/api/nova-lab/modules?q=${query}`)
12    results.value = await response.json()
13  } finally {
14    isLoading.value = false
15  }
16})
17</script>

Callback jest asynchroniczny, a blok try/finally gwarantuje, że isLoading wróci do false nawet po błędzie sieci. Asynchroniczność ma jednak cenę: jeśli starsze zapytanie wróci później niż nowsze, nadpisze świeże wyniki. W produkcyjnym panelu dodaj tu więc sprzątanie z onCleanup i AbortController, tak jak w lekcji o zatrzymywaniu watcherów.

Kalkulator Trajektorii z Historią

Drugi panel liczy wynik działania na prędkości i czasie oraz prowadzi historię obliczeń. Wynik to wartość, więc trafia do computed, a wpis w historii to akcja, więc trafia do watchera. Szablon zawiera dwa pola liczbowe, listę z działaniem i historię.

1<template>
2  <div class="trajectory-calc" style="background: #0a0e27; color: #00ff88;">
3    <label>Prędkość (km/s):</label>
4    <input v-model.number="velocity" type="number" style="background: #1a1e37; color: #00ff88;" />
5
6    <label>Operacja:</label>
7    <select v-model="operation" style="background: #1a1e37; color: #00ff88;">
8      <option value="+">Dodaj</option>
9      <option value="-">Odejmij</option>
10      <option value="*">Mnóż</option>
11      <option value="/">Dziel</option>
12    </select>
13
14    <label>Czas (s):</label>
15    <input v-model.number="time" type="number" style="background: #1a1e37; color: #00ff88;" />
16    <p style="color: #00b4d8;">= {{ result }}</p>
17
18    <h3>Historia obliczeń:</h3>
19    <ul>
20      <li v-for="(entry, i) in history" :key="i" style="color: #00b4d8;">{{ entry }}</li>
21    </ul>
22  </div>
23</template>

Pola używają v-model.number, więc do zmiennych trafiają liczby, a nie tekst, a lista history renderuje wpisy pod wynikiem. Pierwsza część skryptu to stan panelu i computed z wynikiem.

1<script setup>
2import { ref, computed, watch } from 'vue'
3
4const velocity = ref(25)
5const time = ref(3600)
6const operation = ref('*')
7const history = ref([])
8
9const result = computed(() => {
10  switch (operation.value) {
11    case '+': return velocity.value + time.value
12    case '-': return velocity.value - time.value
13    case '*': return velocity.value * time.value
14    case '/': return time.value !== 0 ? velocity.value / time.value : 'Error'
15    default: return 0
16  }
17})

Instrukcja switch wybiera działanie, a przy dzieleniu przez zero computed zwraca napis Error zamiast nieskończoności. Druga część skryptu dopisuje wyniki do historii.

1// Zapisuj do historii obliczeń
2watch(result, (newResult) => {
3  const entry = `${velocity.value} ${operation.value} ${time.value} = ${newResult}`
4  history.value.unshift(entry)
5
6  // Ogranicz do 10 wpisów
7  if (history.value.length > 10) {
8    history.value.pop()
9  }
10})
11</script>

Watcher obserwuje computed result, bo computed jest pełnoprawnym źródłem dla watch. Metoda unshift dopisuje wpis na początek tablicy, a pop usuwa najstarszy, gdy wpisów jest więcej niż 10. Zwróć uwagę na rzecz nieoczywistą: gdy prędkość wynosi 0, zmiana czasu przy mnożeniu nie doda wpisu, bo wynik wciąż wynosi 0, a watcher reaguje tylko na zmianę wartości.

Walidacja Formularza

Trzeci panel to rejestracja z adresem e-mail i hasłem. Tu wystarczą same computed, bo każdy komunikat i stan przycisku to wartość wyliczona z tego, co wpisał użytkownik. Szablon wiąże pola, pokazuje błędy i blokuje przycisk.

1<template>
2  <form @submit.prevent="submit">
3    <div>
4      <input v-model="email" placeholder="Email" />
5      <span v-if="emailError" class="error">{{ emailError }}</span>
6    </div>
7
8    <div>
9      <input v-model="password" type="password" placeholder="Hasło" />
10      <div class="strength" :class="passwordStrengthClass">
11        Siła hasła: {{ passwordStrength }}
12      </div>
13    </div>
14
15    <button :disabled="!isFormValid">Zarejestruj</button>
16  </form>
17</template>

Przycisk dostaje :disabled="!isFormValid", więc odblokuje się sam, gdy formularz stanie się poprawny, a :class przełącza wygląd wskaźnika siły hasła. Pierwsza część skryptu definiuje pola i dwa computed z komunikatami.

1<script setup>
2import { ref, computed } from 'vue'
3
4const email = ref('')
5const password = ref('')
6
7const emailError = computed(() => {
8  if (!email.value) return ''
9  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
10  return regex.test(email.value) ? '' : 'Nieprawidłowy email'
11})
12
13const passwordStrength = computed(() => {
14  const pwd = password.value
15  if (pwd.length < 6) return 'Słabe'
16  if (pwd.length < 10) return 'Średnie'
17  if (/[A-Z]/.test(pwd) && /[0-9]/.test(pwd)) return 'Silne'
18  return 'Średnie'
19})

emailError najpierw sprawdza, czy pole jest puste, bo pusty formularz nie powinien krzyczeć błędem, zanim użytkownik zacznie pisać. Potem wyrażenie regularne testuje, czy adres ma postać "coś@coś.coś" bez spacji. passwordStrength ocenia hasło po długości i obecności wielkiej litery oraz cyfry. Druga część skryptu buduje na tych wynikach.

1const passwordStrengthClass = computed(() => ({
2  weak: passwordStrength.value === 'Słabe',
3  medium: passwordStrength.value === 'Średnie',
4  strong: passwordStrength.value === 'Silne'
5}))
6
7const isFormValid = computed(() => {
8  return email.value &&
9         !emailError.value &&
10         password.value.length >= 6
11})
12
13function submit() {
14  console.log('Formularz wysłany')
15}
16</script>

passwordStrengthClass czyta passwordStrength, a isFormValid czyta emailError - to łańcuch computed, w którym każde ogniwo przelicza się tylko po zmianie ogniwa poprzedniego. Ten sam mechanizm w najprostszej postaci wygląda tak:

1const price = ref(100)
2const tax = computed(() => price.value * 0.23)
3const total = computed(() => price.value + tax.value)

Zmiana price unieważnia oba computed, a total przelicza się z nowym podatkiem przy najbliższym odczycie. Łańcuch nie potrzebuje żadnego watchera ani ręcznej synchronizacji.

Debugowanie computed i watcherów

Gdy computed nie przelicza się wtedy, kiedy oczekujesz, zacznij od Vue DevTools. To rozszerzenie przeglądarki, dostępne też jako wtyczka Vite i osobna aplikacja, które pokazuje drzewo komponentów i stan każdego z nich, w tym bieżące wartości ref i computed. Do precyzyjnego śledzenia służą opcje onTrack i onTrigger, przekazywane jako drugi argument computed albo w opcjach watch.

1const tax = computed(() => price.value * 0.23, {
2  onTrack(event) {
3    console.log('tracked', event.key)
4  },
5  onTrigger(event) {
6    console.log('triggered', event.key, event.newValue)
7  }
8})

onTrack wywoła się, gdy getter zarejestruje zależność, a onTrigger, gdy zmiana tej zależności unieważni wynik, o ile computed jest akurat używany, na przykład w szablonie. Obie opcje działają tylko w trybie deweloperskim, więc w wersji produkcyjnej nie kosztują nic. Polecam najpierw DevTools, a onTrack i onTrigger dopiero wtedy, gdy podgląd stanu nie wystarcza. W projekcie modułu połączysz wszystkie te narzędzia w jednym dashboardzie misji.

Pamiętaj: computed odpowiada na pytanie "ile wynosi", a watch na pytanie "co zrobić, gdy się zmieni" - pomyl te role, a panel misji albo liczy za dużo, albo nie reaguje wcale.

Kod do tej lekcji: App.vue
1<script setup>
2import { ref, watch, watchEffect, onUnmounted } from 'vue'
3
4// NOVA LAB - Advanced Watching: Multiple Sources & Cleanup
5const sensorA = ref(75)
6const sensorB = ref(82)
7const sensorC = ref(91)
8const isMonitoring = ref(false)
9const alerts = ref([])
10const sensorReadings = ref([])
11
12let monitoringInterval = null
13
14// Watch multiple sources
15watch([sensorA, sensorB, sensorC], ([a, b, c], [oldA, oldB, oldC]) => {
16  const timestamp = new Date().toLocaleTimeString()
17
18  if (a !== oldA) {
19    alerts.value.unshift({
20      time: timestamp,
21      sensor: 'A',
22      value: a,
23      previous: oldA,
24      color: '#00ff88'
25    })
26  }
27  if (b !== oldB) {
28    alerts.value.unshift({
29      time: timestamp,
30      sensor: 'B',
31      value: b,
32      previous: oldB,
33      color: '#00b4d8'
34    })
35  }
36  if (c !== oldC) {
37    alerts.value.unshift({
38      time: timestamp,
39      sensor: 'C',
40      value: c,
41      previous: oldC,
42      color: '#ffaa00'
43    })
44  }
45
46  // Calculate average
47  const avg = Math.round((a + b + c) / 3)
48  if (avg < 70) {
49    alerts.value.unshift({
50      time: timestamp,
51      sensor: 'SYSTEM',
52      value: avg,
53      message: 'Average below threshold!',
54      color: '#ff0044'
55    })
56  }
57})
58
59// Watch with cleanup function
60watch(isMonitoring, (newValue, oldValue, onCleanup) => {
61  const timestamp = new Date().toLocaleTimeString()
62
63  if (newValue) {
64    alerts.value.unshift({
65      time: timestamp,
66      message: '▶Continuous monitoring started',
67      color: '#00ff88'
68    })
69
70    // Start interval
71    monitoringInterval = setInterval(() => {
72      const reading = {
73        time: new Date().toLocaleTimeString(),
74        a: sensorA.value,
75        b: sensorB.value,
76        c: sensorC.value,
77        avg: Math.round((sensorA.value + sensorB.value + sensorC.value) / 3)
78      }
79      sensorReadings.value.unshift(reading)
80
81      // Simulate sensor fluctuation
82      sensorA.value = Math.max(0, Math.min(100, sensorA.value + (Math.random() - 0.5) * 2))
83      sensorB.value = Math.max(0, Math.min(100, sensorB.value + (Math.random() - 0.5) * 2))
84      sensorC.value = Math.max(0, Math.min(100, sensorC.value + (Math.random() - 0.5) * 2))
85    }, 2000)
86
87    // Cleanup function - called when watcher stops or before re-run
88    onCleanup(() => {
89      if (monitoringInterval) {
90        clearInterval(monitoringInterval)
91        const stopTime = new Date().toLocaleTimeString()
92        alerts.value.unshift({
93          time: stopTime,
94          message: '⏸Monitoring stopped (cleanup executed)',
95          color: '#ffaa00'
96        })
97      }
98    })
99  }
100})
101
102// watchEffect for computed average
103const avgReading = ref(0)
104watchEffect(() => {
105  avgReading.value = Math.round((sensorA.value + sensorB.value + sensorC.value) / 3)
106})
107
108function adjustSensor(sensor, delta) {
109  if (sensor === 'A') {
110    sensorA.value = Math.max(0, Math.min(100, sensorA.value + delta))
111  } else if (sensor === 'B') {
112    sensorB.value = Math.max(0, Math.min(100, sensorB.value + delta))
113  } else if (sensor === 'C') {
114    sensorC.value = Math.max(0, Math.min(100, sensorC.value + delta))
115  }
116}
117
118function toggleMonitoring() {
119  isMonitoring.value = !isMonitoring.value
120}
121
122function resetSensors() {
123  sensorA.value = 75
124  sensorB.value = 82
125  sensorC.value = 91
126}
127
128function clearAlerts() {
129  alerts.value = []
130  sensorReadings.value = []
131}
132
133// Cleanup on unmount
134onUnmounted(() => {
135  if (monitoringInterval) {
136    clearInterval(monitoringInterval)
137  }
138})
139</script>
140
141<template>
142  <div class="advanced-watch">
143    <div class="header">
144      <h1>NOVA LAB Sensor Array</h1>
145      <p class="subtitle">Multiple Sources & Watch Cleanup</p>
146    </div>
147
148    <div class="monitoring-control">
149      <button
150        @click="toggleMonitoring"
151        :class="{ active: isMonitoring }"
152        class="monitor-btn"
153      >
154        {{ isMonitoring ? '⏸Stop Monitoring' : '▶Start Monitoring' }}
155      </button>
156      <div class="status" :class="{ monitoring: isMonitoring }">
157        {{ isMonitoring ? 'ACTIVE' : 'INACTIVE' }}
158      </div>
159    </div>
160
161    <div class="sensors-grid">
162      <div class="sensor-card sensor-a">
163        <h3>Sensor A</h3>
164        <div class="sensor-visual">
165          <div
166            class="sensor-bar"
167            :style="{ height: sensorA + '%', background: '#00ff88' }"
168          ></div>
169        </div>
170        <div class="sensor-value">{{ sensorA.toFixed(1) }}%</div>
171        <div class="sensor-controls">
172          <button @click="adjustSensor('A', -5)">-5</button>
173          <button @click="adjustSensor('A', 5)">+5</button>
174        </div>
175      </div>
176
177      <div class="sensor-card sensor-b">
178        <h3>Sensor B</h3>
179        <div class="sensor-visual">
180          <div
181            class="sensor-bar"
182            :style="{ height: sensorB + '%', background: '#00b4d8' }"
183          ></div>
184        </div>
185        <div class="sensor-value">{{ sensorB.toFixed(1) }}%</div>
186        <div class="sensor-controls">
187          <button @click="adjustSensor('B', -5)">-5</button>
188          <button @click="adjustSensor('B', 5)">+5</button>
189        </div>
190      </div>
191
192      <div class="sensor-card sensor-c">
193        <h3>Sensor C</h3>
194        <div class="sensor-visual">
195          <div
196            class="sensor-bar"
197            :style="{ height: sensorC + '%', background: '#ffaa00' }"
198          ></div>
199        </div>
200        <div class="sensor-value">{{ sensorC.toFixed(1) }}%</div>
201        <div class="sensor-controls">
202          <button @click="adjustSensor('C', -5)">-5</button>
203          <button @click="adjustSensor('C', 5)">+5</button>
204        </div>
205      </div>
206
207      <div class="sensor-card average">
208        <h3>Average</h3>
209        <div class="avg-display">{{ avgReading }}%</div>
210        <div class="avg-status" :style="{
211          color: avgReading > 80 ? '#00ff88' : avgReading > 60 ? '#ffaa00' : '#ff0044'
212        }">
213          {{ avgReading > 80 ? 'OPTIMAL' : avgReading > 60 ? 'CAUTION' : 'CRITICAL' }}
214        </div>
215      </div>
216    </div>
217
218    <div class="quick-actions">
219      <button @click="resetSensors">Reset Sensors</button>
220      <button @click="clearAlerts">Clear Logs</button>
221    </div>
222
223    <div class="data-panels">
224      <div class="panel alerts-panel">
225        <h3>Alert Log</h3>
226        <div class="panel-content">
227          <div
228            v-for="(alert, index) in alerts.slice(0, 10)"
229            :key="index"
230            class="alert-item"
231            :style="{ borderLeftColor: alert.color }"
232          >
233            <span class="alert-time">[{{ alert.time }}]</span>
234            <span v-if="alert.sensor" class="alert-sensor">Sensor {{ alert.sensor }}:</span>
235            <span v-if="alert.value !== undefined" class="alert-value">
236              {{ alert.previous !== undefined ? `${alert.previous} → ` : '' }}{{ alert.value }}%
237            </span>
238            <span v-if="alert.message" class="alert-message">{{ alert.message }}</span>
239          </div>
240          <div v-if="alerts.length === 0" class="no-data">
241            No alerts yet...
242          </div>
243        </div>
244      </div>
245
246      <div class="panel readings-panel">
247        <h3>Sensor Readings (Auto-capture)</h3>
248        <div class="panel-content">
249          <div
250            v-for="(reading, index) in sensorReadings.slice(0, 8)"
251            :key="index"
252            class="reading-item"
253          >
254            <span class="reading-time">[{{ reading.time }}]</span>
255            <span class="reading-data">
256              A: {{ reading.a.toFixed(1) }}% |
257              B: {{ reading.b.toFixed(1) }}% |
258              C: {{ reading.c.toFixed(1) }}% |
259              Avg: {{ reading.avg }}%
260            </span>
261          </div>
262          <div v-if="sensorReadings.length === 0" class="no-data">
263            Start monitoring to capture readings...
264          </div>
265        </div>
266      </div>
267    </div>
268
269    <div class="info-box">
270      <strong>Advanced Watching:</strong> Watch multiple sources with <code>watch([a, b, c], ...)</code>.
271      Use <code>onCleanup()</code> to clean up side effects like intervals when watcher stops!
272    </div>
273  </div>
274</template>
275
276<style>
277* {
278  margin: 0;
279  padding: 0;
280  box-sizing: border-box;
281}
282
283body {
284  font-family: 'Courier New', monospace;
285  background: #0a0e27;
286  color: #00ff88;
287  padding: 20px;
288}
289
290.advanced-watch {
291  max-width: 1200px;
292  margin: 0 auto;
293}
294
295.header {
296  text-align: center;
297  margin-bottom: 30px;
298  padding: 20px;
299  background: linear-gradient(135deg, #1a1f4c 0%, #0a0e27 100%);
300  border: 2px solid #00ff88;
301  border-radius: 10px;
302}
303
304.header h1 {
305  font-size: 2rem;
306  margin-bottom: 10px;
307  text-shadow: 0 0 20px #00ff88;
308}
309
310.subtitle {
311  color: #00b4d8;
312  font-size: 1.1rem;
313}
314
315.monitoring-control {
316  display: flex;
317  justify-content: center;
318  align-items: center;
319  gap: 20px;
320  margin-bottom: 30px;
321}
322
323.monitor-btn {
324  padding: 15px 40px;
325  font-size: 1.2rem;
326  background: linear-gradient(135deg, #00b4d8 0%, #0077aa 100%);
327  color: white;
328  border: none;
329  border-radius: 8px;
330  cursor: pointer;
331  font-family: 'Courier New', monospace;
332  font-weight: bold;
333  transition: all 0.3s ease;
334}
335
336.monitor-btn.active {
337  background: linear-gradient(135deg, #00ff88 0%, #00cc66 100%);
338  animation: pulse 2s infinite;
339}
340
341@keyframes pulse {
342  0%, 100% { box-shadow: 0 0 20px rgba(0, 255, 136, 0.5); }
343  50% { box-shadow: 0 0 40px rgba(0, 255, 136, 0.8); }
344}
345
346.status {
347  font-size: 1.5rem;
348  font-weight: bold;
349  color: #666;
350}
351
352.status.monitoring {
353  color: #00ff88;
354  text-shadow: 0 0 10px #00ff88;
355}
356
357.sensors-grid {
358  display: grid;
359  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
360  gap: 20px;
361  margin-bottom: 20px;
362}
363
364.sensor-card {
365  background: rgba(0, 180, 216, 0.05);
366  border: 2px solid #00b4d8;
367  border-radius: 10px;
368  padding: 20px;
369  text-align: center;
370}
371
372.sensor-card h3 {
373  color: #00b4d8;
374  margin-bottom: 15px;
375}
376
377.sensor-visual {
378  height: 150px;
379  background: rgba(0, 0, 0, 0.5);
380  border: 2px solid #00ff88;
381  border-radius: 8px;
382  margin-bottom: 15px;
383  display: flex;
384  align-items: flex-end;
385  justify-content: center;
386  overflow: hidden;
387}
388
389.sensor-bar {
390  width: 60%;
391  transition: height 0.3s ease;
392  border-radius: 5px 5px 0 0;
393}
394
395.sensor-value {
396  font-size: 1.8rem;
397  font-weight: bold;
398  color: #00ff88;
399  margin-bottom: 10px;
400}
401
402.sensor-controls {
403  display: flex;
404  gap: 10px;
405  justify-content: center;
406}
407
408.sensor-controls button {
409  background: #00b4d8;
410  color: white;
411  border: none;
412  padding: 8px 15px;
413  border-radius: 5px;
414  cursor: pointer;
415  font-family: 'Courier New', monospace;
416  font-weight: bold;
417}
418
419.average {
420  grid-column: span 1;
421}
422
423.avg-display {
424  font-size: 3rem;
425  font-weight: bold;
426  color: #00ff88;
427  text-shadow: 0 0 20px #00ff88;
428  margin: 20px 0;
429}
430
431.avg-status {
432  font-size: 1.2rem;
433  font-weight: bold;
434}
435
436.quick-actions {
437  display: flex;
438  gap: 15px;
439  justify-content: center;
440  margin-bottom: 20px;
441}
442
443.quick-actions button {
444  padding: 12px 30px;
445  background: linear-gradient(135deg, #666 0%, #444 100%);
446  color: white;
447  border: none;
448  border-radius: 5px;
449  cursor: pointer;
450  font-family: 'Courier New', monospace;
451  font-weight: bold;
452}
453
454.data-panels {
455  display: grid;
456  grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
457  gap: 20px;
458  margin-bottom: 20px;
459}
460
461.panel {
462  background: rgba(0, 0, 0, 0.5);
463  border: 2px solid #00ff88;
464  border-radius: 10px;
465  padding: 20px;
466}
467
468.panel h3 {
469  color: #00ff88;
470  margin-bottom: 15px;
471}
472
473.panel-content {
474  max-height: 300px;
475  overflow-y: auto;
476}
477
478.alert-item,
479.reading-item {
480  padding: 10px;
481  margin-bottom: 8px;
482  background: rgba(0, 255, 136, 0.05);
483  border-radius: 4px;
484  font-size: 0.9rem;
485  animation: slideIn 0.3s ease;
486}
487
488.alert-item {
489  border-left: 4px solid;
490}
491
492@keyframes slideIn {
493  from {
494    opacity: 0;
495    transform: translateX(-10px);
496  }
497  to {
498    opacity: 1;
499    transform: translateX(0);
500  }
501}
502
503.alert-time,
504.reading-time {
505  opacity: 0.7;
506  margin-right: 8px;
507}
508
509.alert-sensor {
510  color: #00b4d8;
511  font-weight: bold;
512  margin-right: 5px;
513}
514
515.alert-value {
516  color: #00ff88;
517}
518
519.reading-data {
520  color: #00b4d8;
521}
522
523.no-data {
524  text-align: center;
525  padding: 30px;
526  color: #666;
527  font-style: italic;
528}
529
530.info-box {
531  background: rgba(0, 255, 136, 0.1);
532  border: 2px solid #00ff88;
533  border-radius: 10px;
534  padding: 15px;
535  text-align: center;
536  color: #00ff88;
537  line-height: 1.6;
538}
539
540code {
541  background: rgba(0, 0, 0, 0.3);
542  padding: 2px 8px;
543  border-radius: 3px;
544  color: #00b4d8;
545}
546</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. Kiedy lepiej użyć computed zamiast watch?

  2. 2. Kiedy lepiej użyć watch zamiast computed?

To 2 z 10 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Uzupełnij debounced search używając watch do opóźnienia wysyłania zapytania.

  • Klikanie w kolejności

    Ułóż poprawny import computed i watch z Vue:

  • Układanie w pionie

    Ułóż tryby flush watch od najwcześniejszego do najpóźniejszego wykonania:

  • Układanie w poziomie

    Ułóż watch obserwujący property reactive obiektu:

  • Klikanie w kolejności

    Ułóż watch obserwujący wiele refs jednocześnie:

  • Układanie w pionie

    Ułóż kroki tworzenia computed property od początku do końca:

  • Układanie w poziomie

    Ułóż poprawną destrukturyzację w watch na wiele źródeł:

  • Klikanie w kolejności

    Ułóż watchEffect z onCleanup w prawidłowej kolejności:

  • Układanie w poziomie

    Ułóż computed do walidacji email w prawidłowej kolejności:

  • Układanie w pionie

    Ułóż kroki implementacji debounce z watch od początku do końca:

  • Klikanie w kolejności

    Ułóż łańcuch computed dependencies od źródła do końcowej wartości:

  • Układanie w poziomie

    Ułóż poprawne wywołanie watchPostEffect:

Przydatne artykuły