Kurs Vue.js · Moduł 4: Computed i watch
Computed i Watch w Praktyce - Systemy Misji
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. Kiedy lepiej użyć computed zamiast watch?
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: