Cours Vue.js · Module 4: Computed et watch

Propriétés calculées (computed) - calculs automatiques

7 min de lecture
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. 1. Qu'est-ce qu'une propriété calculée (computed) dans Vue.js ?

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