Kurs Vue.js · Moduł 3: Reaktywność

Głęboka vs Płytka Reaktywność

4 min czytania
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 kontrolnego

Każ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:

SytuacjaUżyj
Duże obiekty z APIshallowRef
Biblioteki zewnętrzneshallowRef
Optymalizacja wydajnościshallow*
Typowe dane UIref/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. 1. Jaka jest domyślna głębokość reaktywności ref() i reactive()?

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

Przydatne artykuły