Kurs Vue.js · Moduł 3: Reaktywność
Głęboka vs Płytka Reaktywność
W tej lekcji5
Vue oferuje różne poziomy reaktywności - jak różne warstwy sensorów w reaktorze.
Telemetria reaktora przysyła co kilka sekund paczkę z tysiącami odczytów. Za każdym razem podmieniasz całą paczkę, a mimo to domyślna reaktywność opakowuje w Proxy każdy zagnieżdżony obiekt, do którego sięgniesz, i śledzi każdy odczyt z osobna. To tak, jakby montować czujnik na każdej śrubie ładunku, choć interesuje Cię tylko to, czy przywieziono nową skrzynię. Na takie sytuacje Vue ma wersje płytkie.
Deep Reactivity (domyślnie)
Domyślnie reaktywność jest głęboka: ref() z obiektem i reactive() śledzą zmiany na każdym poziomie zagnieżdżenia, także w tablicach.
1import { ref, reactive } from 'vue'
2
3// ref z obiektem - głęboka reaktywność
4const missionControl = ref({
5 station: 'NOVA LAB',
6 modules: [
7 { id: 1, name: 'Oxygen Generator' },
8 { id: 2, name: 'Water Recycler' }
9 ]
10})
11
12// Zmiany na każdym poziomie są reaktywne
13missionControl.value.station = 'NOVA LAB Alpha'
14missionControl.value.modules[0].name = 'Oxygen Generator Prime'
15missionControl.value.modules.push({ id: 3, name: 'Power Core' })
16// Wszystkie powyższe wywołają aktualizację panelu kontrolnegoKażda z trzech zmian odświeży panel, bo ref() przekazał obiekt do reactive(), a ten opakowuje zagnieżdżone obiekty, gdy tylko po nie sięgniesz. Dla typowych danych interfejsu to wygodne i wystarczająco szybkie.
shallowRef() - Płytka Reaktywność
shallowRef() przechowuje wartość bez przerabiania jej na Proxy. Reaktywny jest wyłącznie dostęp do .value, czyli podmiana całej zawartości:
1import { shallowRef } from 'vue'
2
3const missionControl = shallowRef({
4 station: 'NOVA LAB',
5 modules: [{ id: 1, name: 'Oxygen Generator' }]
6})
7
8// Tylko zamiana .value jest reaktywna
9missionControl.value.station = 'Nowa nazwa' // NIE wywoła aktualizacji!
10missionControl.value.modules.push({ id: 2, name: 'Water Recycler' }) // NIE wywoła aktualizacji!
11
12// TO zadziała - zamiana całego obiektu
13missionControl.value = {
14 station: 'NOVA LAB Beta',
15 modules: []
16}Dwie pierwsze zmiany naprawdę zapisują się w pamięci - nie zmienia się tylko ekran, bo Vue o nich nie wie. Pojawią się dopiero przy najbliższym renderze wywołanym przez coś innego. Żeby zmienić jedno pole w shallowRef o nazwie sensorData, zbuduj nowy obiekt operatorem rozproszenia:
1sensorData.value = {
2 ...sensorData.value,
3 temperature: 3000
4}Nowy obiekt przechodzi przez setter .value, więc widok się odświeży, a stara paczka danych zostaje nietknięta. Kopiowanie ogromnej tablicy tylko po to, by dopisać jeden element, bywa jednak marnotrawstwem. Wtedy zmieniasz zawartość w miejscu i wołasz triggerRef(), który ręcznie uruchamia wszystko, co zależy od płytkiego refa:
1import { shallowRef, triggerRef } from 'vue'
2
3const readings = shallowRef([])
4
5readings.value.push({ id: 1, temperature: 2500 }) // widok jeszcze nie wie
6triggerRef(readings) // wymuś aktualizacjęPo triggerRef widok odświeży się raz. Odpowiedzialność za to wywołanie spoczywa na Tobie - zapomnisz o nim, a panel pokaże stare dane.
shallowReactive()
shallowReactive() robi reaktywne tylko właściwości pierwszego poziomu. Zagnieżdżone obiekty są przechowywane bez zmian, więc Vue nie widzi ich wnętrza:
1import { shallowReactive } from 'vue'
2
3const missionControl = shallowReactive({
4 station: 'NOVA LAB',
5 location: {
6 planet: 'Mars',
7 year: 2087
8 }
9})
10
11missionControl.station = 'Nowa stacja' // Reaktywne - poziom 1
12missionControl.location.planet = 'Europa' // NIE reaktywne - poziom 2
13
14// Musisz zastąpić cały obiekt location
15missionControl.location = { planet: 'Europa', year: 2087 }Podmiana location to zmiana właściwości pierwszego poziomu, dlatego działa. Dokumentacja ostrzega: struktury płytkie stosuj tylko jako główny stan komponentu i nie zagnieżdżaj ich w obiektach głęboko reaktywnych, bo powstaje drzewo o niespójnym zachowaniu.
Kiedy używać shallow?
Wersje płytkie to narzędzie optymalizacji, a nie nowy domyślny wybór:
| Sytuacja | Użyj |
|---|---|
| Duże obiekty z API | shallowRef |
| Biblioteki zewnętrzne | shallowRef |
| Optymalizacja wydajności | shallow* |
| Typowe dane UI | ref/reactive |
Moja rada: zaczynaj od ref i reactive, a po wersje płytkie sięgaj przy dużych, niezmiennych danych albo stanie zarządzanym przez zewnętrzną bibliotekę.
Praktyczny Przykład
Panel monitoringu pobiera dane z sensorów i zawsze podmienia całą paczkę. Szablon pokazuje nazwę systemu i przycisk odświeżania:
1<template>
2 <div class="sensor-display" style="background: #0a0e27; color: #00ff88;">
3 <h2>{{ sensorData.systemName }}</h2>
4 <button @click="refreshData">Odśwież odczyty</button>
5 </div>
6</template>Skrypt trzyma dane w shallowRef, a funkcja asynchroniczna pobiera nowe przez fetch, przeglądarkowe API do zapytań sieciowych:
1<script setup>
2import { shallowRef } from 'vue'
3
4// Duże dane z sensorów - płytka reaktywność
5const sensorData = shallowRef({
6 systemName: 'Reactor Monitoring',
7 readings: [] // Tysiące odczytów z sensorów
8})
9
10async function refreshData() {
11 const response = await fetch('/api/sensors')
12 // Zamiana całego obiektu - reaktywne
13 sensorData.value = await response.json()
14}
15</script>await czeka na odpowiedź, a potem przypisanie do .value odświeża panel. Vue nie opakowuje tysięcy odczytów, bo nikt ich pojedynczo nie modyfikuje. W następnej lekcji wyciągniesz pojedyncze właściwości z obiektu reaktywnego, nie zrywając połączenia.
Zapamiętaj: głęboka reaktywność to czujnik na każdej śrubie, a płytka to jeden czujnik na włazie - zauważy tylko wymianę całego ładunku albo ręczny sygnał triggerRef.
Kod do tej lekcji: App.vue
1<script setup>
2import { reactive, toRef, toRefs } from 'vue'
3
4// toRef() i toRefs() - Ekstrakcja reaktywnych właściwości
5// System zarządzania członkami załogi NOVA LAB
6
7const crewMember = reactive({
8 id: 'CREW-001',
9 name: 'Dr. Sarah Chen',
10 role: 'Chief Engineer',
11 status: 'ACTIVE',
12 stats: {
13 health: 95,
14 energy: 80,
15 morale: 90
16 },
17 certifications: ['Reactor Operations', 'EVA Specialist', 'Life Support'],
18 missions: 12
19})
20
21// toRef() - tworzy ref do pojedynczej właściwości
22// Pozostaje reaktywna i zsynchronizowana z oryginalnym obiektem
23const memberName = toRef(crewMember, 'name')
24const memberStatus = toRef(crewMember, 'status')
25const memberHealth = toRef(crewMember.stats, 'health')
26
27// toRefs() - tworzy refs do wszystkich właściwości obiektu
28// Przydatne przy destructuring
29const { role, missions } = toRefs(crewMember)
30
31// Drugi członek załogi dla demonstracji
32const secondCrew = reactive({
33 id: 'CREW-002',
34 name: 'Cpt. Marcus Rodriguez',
35 role: 'Mission Commander',
36 status: 'ACTIVE',
37 stats: {
38 health: 88,
39 energy: 75,
40 morale: 85
41 },
42 certifications: ['Command', 'Navigation', 'Emergency Response'],
43 missions: 25
44})
45
46// Używamy toRefs() do destrukturyzacji
47const {
48 name: secondName,
49 role: secondRole,
50 missions: secondMissions,
51 status: secondStatus
52} = toRefs(secondCrew)
53
54// Funkcje do modyfikacji
55function updateStatus(newStatus) {
56 // Modyfikacja przez toRef - zmienia oryginalny obiekt
57 memberStatus.value = newStatus
58}
59
60function decreaseHealth() {
61 // Modyfikacja przez toRef - wpływa na oryginalny obiekt
62 if (memberHealth.value > 0) {
63 memberHealth.value -= 10
64 }
65}
66
67function increaseMissions() {
68 // Modyfikacja przez toRefs - wpływa na oryginalny obiekt
69 missions.value++
70}
71
72function changeRole(newRole) {
73 // Modyfikacja przez toRefs - wpływa na oryginalny obiekt
74 role.value = newRole
75}
76
77function promoteSecondCrew() {
78 secondMissions.value++
79 if (secondMissions.value > 30) {
80 secondRole.value = 'Senior Commander'
81 secondStatus.value = 'VETERAN'
82 }
83}
84</script>
85
86<template>
87 <div class="crew-management">
88 <div class="header">
89 <h1>ZARZĄDZANIE ZAŁOGĄ - NOVA LAB</h1>
90 <p class="subtitle">toRef() i toRefs() - Ekstrakcja reaktywnych właściwości</p>
91 </div>
92
93 <div class="crew-grid">
94 <!-- Pierwszy członek załogi - toRef() -->
95 <div class="crew-card">
96 <div class="crew-header">
97 <h2>{{ crewMember.name }}</h2>
98 <span :class="'status-badge status-' + crewMember.status.toLowerCase()">
99 {{ crewMember.status }}
100 </span>
101 </div>
102
103 <div class="crew-info">
104 <p><strong>ID:</strong> {{ crewMember.id }}</p>
105 <p><strong>Rola:</strong> {{ crewMember.role }}</p>
106 <p><strong>Misje:</strong> {{ crewMember.missions }}</p>
107 </div>
108
109 <div class="stats-section">
110 <h3>Statystyki</h3>
111 <div class="stat-bar">
112 <label>Zdrowie</label>
113 <div class="progress">
114 <div class="progress-fill health" :style="{ width: crewMember.stats.health + '%' }">
115 {{ crewMember.stats.health }}%
116 </div>
117 </div>
118 </div>
119 <div class="stat-bar">
120 <label>Energia</label>
121 <div class="progress">
122 <div class="progress-fill energy" :style="{ width: crewMember.stats.energy + '%' }">
123 {{ crewMember.stats.energy }}%
124 </div>
125 </div>
126 </div>
127 <div class="stat-bar">
128 <label>Morale</label>
129 <div class="progress">
130 <div class="progress-fill morale" :style="{ width: crewMember.stats.morale + '%' }">
131 {{ crewMember.stats.morale }}%
132 </div>
133 </div>
134 </div>
135 </div>
136
137 <div class="certifications">
138 <h3>Certyfikaty</h3>
139 <div class="cert-tags">
140 <span v-for="cert in crewMember.certifications" :key="cert" class="cert-tag">
141 {{ cert }}
142 </span>
143 </div>
144 </div>
145
146 <div class="controls">
147 <button @click="updateStatus('ON_MISSION')" class="btn btn-small">
148 Wyślij na misję
149 </button>
150 <button @click="updateStatus('RESTING')" class="btn btn-small">
151 Odpoczynek
152 </button>
153 <button @click="decreaseHealth()" class="btn btn-small btn-warning">
154 Symuluj uraz
155 </button>
156 <button @click="increaseMissions()" class="btn btn-small btn-success">
157 Dodaj misję
158 </button>
159 </div>
160
161 <div class="code-box">
162 <pre>// toRef() - pojedyncza właściwość
163const name = toRef(crewMember, 'name')
164const health = toRef(crewMember.stats, 'health')
165
166// Modyfikacja
167health.value -= 10</pre>
168 </div>
169 </div>
170
171 <!-- Drugi członek załogi - toRefs() -->
172 <div class="crew-card">
173 <div class="crew-header">
174 <h2>{{ secondCrew.name }}</h2>
175 <span :class="'status-badge status-' + secondCrew.status.toLowerCase()">
176 {{ secondCrew.status }}
177 </span>
178 </div>
179
180 <div class="crew-info">
181 <p><strong>ID:</strong> {{ secondCrew.id }}</p>
182 <p><strong>Rola:</strong> {{ secondRole }}</p>
183 <p><strong>Misje:</strong> {{ secondMissions }}</p>
184 </div>
185
186 <div class="stats-section">
187 <h3>Statystyki</h3>
188 <div class="stat-bar">
189 <label>Zdrowie</label>
190 <div class="progress">
191 <div class="progress-fill health" :style="{ width: secondCrew.stats.health + '%' }">
192 {{ secondCrew.stats.health }}%
193 </div>
194 </div>
195 </div>
196 <div class="stat-bar">
197 <label>Energia</label>
198 <div class="progress">
199 <div class="progress-fill energy" :style="{ width: secondCrew.stats.energy + '%' }">
200 {{ secondCrew.stats.energy }}%
201 </div>
202 </div>
203 </div>
204 <div class="stat-bar">
205 <label>Morale</label>
206 <div class="progress">
207 <div class="progress-fill morale" :style="{ width: secondCrew.stats.morale + '%' }">
208 {{ secondCrew.stats.morale }}%
209 </div>
210 </div>
211 </div>
212 </div>
213
214 <div class="certifications">
215 <h3>Certyfikaty</h3>
216 <div class="cert-tags">
217 <span v-for="cert in secondCrew.certifications" :key="cert" class="cert-tag">
218 {{ cert }}
219 </span>
220 </div>
221 </div>
222
223 <div class="controls">
224 <button @click="promoteSecondCrew()" class="btn btn-small btn-success">
225 Awansuj
226 </button>
227 <button @click="changeRole('Flight Director')" class="btn btn-small">
228 Zmień rolę
229 </button>
230 <button @click="secondStatus.value = 'ACTIVE'" class="btn btn-small">
231 Aktywuj
232 </button>
233 </div>
234
235 <div class="code-box">
236 <pre>// toRefs() - wszystkie właściwości
237const { role, missions, status } = toRefs(secondCrew)
238
239// Modyfikacja
240missions.value++
241role.value = 'Senior Commander'</pre>
242 </div>
243 </div>
244 </div>
245
246 <div class="info-box">
247 <h3>Różnica między toRef() i toRefs()</h3>
248 <div class="comparison-grid">
249 <div class="comparison-item">
250 <h4>toRef(object, 'key')</h4>
251 <ul>
252 <li>Tworzy ref do <strong>pojedynczej</strong> właściwości</li>
253 <li>Przydatne gdy potrzebujemy tylko jednej wartości</li>
254 <li>Zachowuje reaktywność z oryginalnym obiektem</li>
255 <li>Przykład: <code>const name = toRef(user, 'name')</code></li>
256 </ul>
257 </div>
258 <div class="comparison-item">
259 <h4>toRefs(object)</h4>
260 <ul>
261 <li>Tworzy refs do <strong>wszystkich</strong> właściwości</li>
262 <li>Przydatne przy destructuring</li>
263 <li>Każda właściwość staje się ref</li>
264 <li>Przykład: <code>const { name, age } = toRefs(user)</code></li>
265 </ul>
266 </div>
267 </div>
268 <div class="warning-box">
269 <p><strong>Ważne:</strong> Zwykły destructuring straci reaktywność!</p>
270 <code>const { name } = crewMember // Nie reaktywne</code><br>
271 <code>const { name } = toRefs(crewMember) // Reaktywne</code>
272 </div>
273 </div>
274 </div>
275</template>
276
277<style scoped>
278.crew-management {
279 min-height: 100vh;
280 background: #0a0e27;
281 color: #e0e0e0;
282 padding: 2rem;
283 font-family: 'Courier New', monospace;
284}
285
286.header {
287 text-align: center;
288 margin-bottom: 2rem;
289 border-bottom: 2px solid #00ff88;
290 padding-bottom: 1rem;
291}
292
293.header h1 {
294 color: #00ff88;
295 font-size: 2rem;
296 margin: 0;
297 text-shadow: 0 0 10px #00ff88;
298}
299
300.subtitle {
301 color: #00b4d8;
302 margin: 0.5rem 0 0 0;
303}
304
305.crew-grid {
306 display: grid;
307 grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
308 gap: 2rem;
309 margin-bottom: 2rem;
310}
311
312.crew-card {
313 background: #1a1f3a;
314 padding: 1.5rem;
315 border-radius: 8px;
316 border: 1px solid #00b4d8;
317}
318
319.crew-header {
320 display: flex;
321 justify-content: space-between;
322 align-items: center;
323 margin-bottom: 1rem;
324 padding-bottom: 1rem;
325 border-bottom: 1px solid #333;
326}
327
328.crew-header h2 {
329 color: #00ff88;
330 margin: 0;
331 font-size: 1.3rem;
332}
333
334.status-badge {
335 padding: 0.3rem 0.8rem;
336 border-radius: 20px;
337 font-size: 0.8rem;
338 font-weight: bold;
339}
340
341.status-active {
342 background: #00ff88;
343 color: #0a0e27;
344}
345
346.status-on_mission {
347 background: #00b4d8;
348 color: #0a0e27;
349}
350
351.status-resting {
352 background: #ffaa00;
353 color: #0a0e27;
354}
355
356.status-veteran {
357 background: linear-gradient(135deg, #ffd700, #ffaa00);
358 color: #0a0e27;
359}
360
361.crew-info p {
362 margin: 0.5rem 0;
363 color: #ccc;
364}
365
366.stats-section {
367 margin: 1.5rem 0;
368}
369
370.stats-section h3 {
371 color: #00b4d8;
372 font-size: 1rem;
373 margin-bottom: 1rem;
374}
375
376.stat-bar {
377 margin-bottom: 1rem;
378}
379
380.stat-bar label {
381 display: block;
382 font-size: 0.9rem;
383 color: #888;
384 margin-bottom: 0.3rem;
385}
386
387.progress {
388 height: 24px;
389 background: #0a0e27;
390 border-radius: 4px;
391 overflow: hidden;
392}
393
394.progress-fill {
395 height: 100%;
396 display: flex;
397 align-items: center;
398 justify-content: center;
399 color: #0a0e27;
400 font-weight: bold;
401 font-size: 0.85rem;
402 transition: width 0.3s ease;
403}
404
405.progress-fill.health {
406 background: linear-gradient(90deg, #ff4444, #ff6666);
407}
408
409.progress-fill.energy {
410 background: linear-gradient(90deg, #00b4d8, #00d4ff);
411}
412
413.progress-fill.morale {
414 background: linear-gradient(90deg, #00ff88, #44ffaa);
415}
416
417.certifications {
418 margin: 1.5rem 0;
419}
420
421.certifications h3 {
422 color: #00b4d8;
423 font-size: 1rem;
424 margin-bottom: 0.8rem;
425}
426
427.cert-tags {
428 display: flex;
429 flex-wrap: wrap;
430 gap: 0.5rem;
431}
432
433.cert-tag {
434 background: #0a0e27;
435 padding: 0.4rem 0.8rem;
436 border-radius: 4px;
437 border: 1px solid #00ff88;
438 color: #00ff88;
439 font-size: 0.85rem;
440}
441
442.controls {
443 display: flex;
444 flex-wrap: wrap;
445 gap: 0.5rem;
446 margin: 1rem 0;
447}
448
449.btn {
450 padding: 0.6rem 1rem;
451 font-size: 0.9rem;
452 border: none;
453 border-radius: 6px;
454 cursor: pointer;
455 font-weight: bold;
456 transition: all 0.3s ease;
457 font-family: 'Courier New', monospace;
458 background: #00b4d8;
459 color: #0a0e27;
460}
461
462.btn-small {
463 font-size: 0.85rem;
464 padding: 0.5rem 0.8rem;
465}
466
467.btn-success {
468 background: linear-gradient(135deg, #00ff88, #00dd77);
469}
470
471.btn-warning {
472 background: linear-gradient(135deg, #ff6666, #ff4444);
473 color: white;
474}
475
476.btn:hover {
477 transform: scale(1.05);
478 box-shadow: 0 0 15px currentColor;
479}
480
481.code-box {
482 background: #0a0e27;
483 padding: 1rem;
484 border-radius: 4px;
485 border-left: 3px solid #00ff88;
486 margin-top: 1rem;
487}
488
489.code-box pre {
490 margin: 0;
491 color: #00ff88;
492 font-size: 0.85rem;
493 overflow-x: auto;
494}
495
496.info-box {
497 background: #1a1f3a;
498 padding: 1.5rem;
499 border-radius: 8px;
500 border-left: 4px solid #00ff88;
501}
502
503.info-box h3 {
504 color: #00ff88;
505 margin-top: 0;
506}
507
508.comparison-grid {
509 display: grid;
510 grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
511 gap: 1.5rem;
512 margin: 1rem 0;
513}
514
515.comparison-item h4 {
516 color: #00b4d8;
517 margin-top: 0;
518}
519
520.comparison-item ul {
521 padding-left: 1.5rem;
522 color: #ccc;
523}
524
525.comparison-item li {
526 margin: 0.5rem 0;
527}
528
529.comparison-item code {
530 background: #0a0e27;
531 padding: 0.2rem 0.5rem;
532 border-radius: 3px;
533 color: #00ff88;
534}
535
536.warning-box {
537 background: #2a1f1a;
538 padding: 1rem;
539 border-radius: 4px;
540 border-left: 3px solid #ffaa00;
541 margin-top: 1rem;
542}
543
544.warning-box code {
545 background: #0a0e27;
546 padding: 0.3rem 0.6rem;
547 border-radius: 3px;
548 color: #00ff88;
549 display: inline-block;
550 margin: 0.2rem 0;
551}
552</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. Jaka jest domyślna głębokość reaktywności ref() i reactive()?
2. Czym różni się shallowRef() od ref()?
To 2 z 4 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Stwórz shallowRef() dla dużego obiektu i pokaż różnicę z ref().
- Układanie w poziomie
Ułóż poprawny sposób aktualizacji shallowRef:
- Klikanie w kolejności
Ułóż od najpłytszej do najgłębszej reaktywności:
- Edytor kodu
Stwórz shallowReactive() i pokaż, że zagnieżdżone zmiany nie wywołują re-renderu.