Cours Vue.js · Module 4: Computed et watch
Propriétés calculées (computed) - calculs automatiques
Dans cette leçon5
Bienvenue au Centre de Calcul de NOVA LAB ! Le navigateur de quart a deux nombres sur son panneau : la vitesse du vaisseau et le temps de vol. Le troisième, la distance parcourue, il le recalcule à la main après chaque correction de cap. Il suffit d'une mise à jour oubliée pour que le panneau affiche une distance vieille d'une heure, sans que personne ne s'en aperçoive. Dr Nova a une règle simple pour cela : une valeur qu'on peut déduire d'autres données doit se calculer toute seule. Dans Vue, ce rôle revient aux propriétés calculées, les computed properties.
Ce que tu vas apprendre
- computed - des valeurs dérivées qui se recalculent seules et mémorisent leur résultat.
- computed avec getter et setter - des champs bidirectionnels, par exemple un convertisseur d'unités d'énergie.
- watch - réagir aux changements : une entrée dans le journal, une requête au serveur, une alarme.
- watchEffect - un watcher qui détecte lui-même les données qu'il utilise.
- arrêter les watchers et faire le ménage - fermer les canaux de télémétrie quand ils ne servent plus.
- la pratique et un projet - un tableau de bord de mission qui réunit tous ces outils.
Qu'est-ce qu'une propriété calculée ?
Tu importes la fonction computed depuis le paquet vue, exactement comme ref. Tu lui passes un getter, c'est-à-dire une fonction fléchée qui retourne le résultat d'un calcul. En échange, tu obtiens un objet semblable à un ref : dans le script, tu lis son résultat avec .value, et dans le template, simplement par son nom. Voici un panneau de navigation avec deux données d'entrée dans des ref et une computed qui calcule la distance.
1<template>
2 <div class="navigation-panel" style="background: #0a0e27; color: #00ff88;">
3 <p>Velocity: {{ velocity }} km/s</p>
4 <p>Time: {{ timeElapsed }} s</p>
5 <p>Distance: {{ 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 - recalcule automatiquement la distance
16const distanceTraveled = computed(() => {
17 return velocity.value * timeElapsed.value
18})
19</script>Le getter multiplie velocity.value par timeElapsed.value, et Vue retient quelles valeurs réactives il a lues au passage. Ce sont les dépendances de la computed. Quand tu fais passer la vitesse de 25 à 30, le paragraphe de la distance affiche 108000 sans la moindre ligne de code en plus. Remarque ce qui n'a pas changé : velocity et timeElapsed restent des ref ordinaires, et la computed n'y écrit rien, elle se contente de les lire. Tu ne peux pas non plus affecter une valeur à la computed elle-même : par défaut, elle est en lecture seule, et une tentative d'écriture se termine par un avertissement dans la console. Le schéma à connaître par cœur ressemble toujours à ceci : const total = computed(() => a.value + b.value).
Computed vs méthodes
Tu pourrais obtenir le même texte avec une simple fonction appelée dans le template. Les deux chemins affichent un texte identique sur le panneau, la différence est donc ailleurs : dans le moment où le code s'exécute. Voici un indicateur de carburant construit des deux façons, avec un console.log dans chacune pour voir chaque exécution.
1<template>
2 <div class="fuel-display" style="background: #0a0e27; color: #00ff88;">
3 <!-- Computed - appelée sans () -->
4 <p>{{ fuelStatus }}</p>
5
6 <!-- Méthode - appelée avec () -->
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 - mise en cache, recalculée uniquement quand les dépendances changent
18const fuelStatus = computed(() => {
19 console.log('Computed exécutée')
20 const percentage = (fuelLevel.value / maxFuel.value) * 100
21 return `Fuel: ${percentage.toFixed(1)}%`
22})
23
24// Méthode - exécutée à chaque rendu
25function getFuelStatus() {
26 console.log('Méthode exécutée')
27 const percentage = (fuelLevel.value / maxFuel.value) * 100
28 return `Fuel: ${percentage.toFixed(1)}%`
29}
30</script>Dans le template, tu écris une computed sans parenthèses, car c'est une valeur, et une méthode avec parenthèses, car c'est un appel. Une méthode s'exécute à chaque rendu du composant, même quand ce rendu a été provoqué par le changement d'un tout autre champ. Une computed ne relance son getter que lorsque fuelLevel ou maxFuel change, et pour tous les autres rendus elle rend le résultat mémorisé. Profitons-en pour démonter trois mythes. Une computed n'est pas "toujours plus rapide" : tout le gain vient de la mémorisation du résultat. Une computed ne prend pas d'arguments : quand tu as besoin d'un paramètre, utilise une méthode. Et une computed ne fonctionne pas de manière asynchrone : le getter doit retourner une valeur tout de suite, donc await et les requêtes au serveur n'y ont pas leur place.
Mise en cache des computed
Le résultat mémorisé, c'est le cache. Il est le plus rentable pour les calculs coûteux, par exemple la moyenne de centaines de relevés de capteurs. Dans l'exemple, la méthode de tableau reduce parcourt les relevés et les additionne en un seul nombre, que l'on divise ensuite par le nombre de relevés.
1<script setup>
2import { ref, computed } from 'vue'
3
4const sensorReadings = ref([25, 30, 28, 32, 29])
5
6// Opération coûteuse - mais mise en cache !
7const averageTemperature = computed(() => {
8 console.log('Calcul de la température moyenne...')
9 return sensorReadings.value.reduce((sum, reading) => {
10 // Simulation d'une opération coûteuse
11 return sum + reading
12 }, 0) / sensorReadings.value.length
13})
14
15// Plusieurs références - un seul calcul
16// {{ averageTemperature }}
17// {{ averageTemperature }}
18// {{ averageTemperature }}
19</script>Les trois commentaires avec {{ averageTemperature }} représentent trois endroits du template qui affichent la moyenne. Malgré trois références, le message sur le calcul de la moyenne n'apparaît qu'une fois dans la console. Et que se passe-t-il quand un nouveau relevé arrive dans sensorReadings ? Vue ne recalcule pas la moyenne immédiatement. Il marque d'abord la computed comme périmée - dans le code source de Vue, ce drapeau s'appelle "dirty". Le getter ne s'exécute qu'à la prochaine lecture de la valeur, et le nouveau résultat part dans le cache avant d'être retourné. Si personne ne lit la moyenne, le getter ne s'exécute pas du tout. Retiens aussi que seule une donnée réactive peut être une dépendance : une computed basée sur Date.now() ne se rafraîchirait jamais.
Filtrer la liste des modules
Une tâche typique pour une computed, c'est une liste filtrée selon ce que l'opérateur a tapé dans le champ de recherche. La méthode filter ne garde que les éléments du tableau qui remplissent une condition, toLowerCase met le texte en minuscules, et includes vérifie si un nom contient le fragment recherché.
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})Cette computed a deux dépendances, searchQuery et modules, donc la liste se rafraîchit aussi bien après la frappe d'une lettre qu'après l'ajout d'un nouveau module. Comme les deux côtés passent en minuscules, la casse ne compte plus. Le tableau d'origine ne change pas, car filter en retourne toujours un nouveau, et un champ de recherche vide laisse passer tous les modules, puisque chaque nom contient le fragment vide.
Je te recommande une règle simple : tout ce qui peut se déduire d'autres données va dans une computed, et non dans un ref séparé que tu mets à jour à la main. Une seule source de vérité, c'est moins d'endroits où l'on peut oublier une mise à jour. Dans la prochaine leçon, tu apprendras à une computed à accepter l'écriture, puis tu découvriras watch, l'outil des moments où il te faut une action plutôt qu'une valeur.
Souviens-toi : une computed est l'ordinateur de navigation de la station - elle ne recalcule le cap que lorsqu'un des paramètres change, et entre deux changements elle donne le résultat mémorisé.
Code de cette leçon : App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4// NOVA LAB Control Center - Statistiques de mission
5const missionDays = ref(127)
6const crewMembers = ref(8)
7const oxygenLevel = ref(87)
8const powerOutput = ref(2400)
9const waterReserves = ref(3200)
10
11// Propriétés calculées - recalculées automatiquement
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 }} (Semaine {{ missionWeeks }})</div>
56 </div>
57
58 <div class="status-panel" :style="{ borderColor: statusColor }">
59 <h2>État du système : <span :style="{ color: statusColor }">{{ systemStatus }}</span></h2>
60 <div class="crew-info">Équipage : {{ crewMembers }} membres</div>
61 </div>
62
63 <div class="stats-grid">
64 <div class="stat-card">
65 <div class="stat-label">Niveau d'oxygène</div>
66 <div class="stat-value">{{ oxygenLevel }}%</div>
67 <div class="stat-computed">{{ oxygenPerPerson }}% par personne</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">Production d'énergie</div>
76 <div class="stat-value">{{ powerOutput }}W</div>
77 <div class="stat-computed">{{ powerPerPerson }}W par personne</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">Réserves d'eau</div>
86 <div class="stat-value">{{ waterReserves }}L</div>
87 <div class="stat-computed">{{ (waterReserves / crewMembers).toFixed(1) }}L par personne</div>
88 </div>
89 </div>
90
91 <div class="info-box">
92 <strong>Propriétés calculées :</strong> les valeurs se mettent à jour automatiquement quand les dépendances changent !
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>Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Qu'est-ce qu'une propriété calculée (computed) dans Vue.js ?
2. Quel est le principal avantage des propriétés calculées par rapport aux méthodes classiques ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Complète le code - crée une propriété calculée qui calcule la distance à partir de la vitesse et du temps.
- Clique dans l'ordre
Ordonne la syntaxe correcte pour créer une propriété calculée :
- Éditeur de code
Complète la propriété calculée qui filtre une liste de modules à partir de la requête saisie.
- Tri vertical
Ordonne les étapes du fonctionnement d'une computed, du changement de donnée jusqu'au résultat :