Cours Vue.js · Module 4: Computed et watch

Computed et Watch en pratique - systèmes de mission

8 min de lecture
Dans cette leçon4

Le Centre de Contrôle a commandé trois panneaux : une recherche de modules qui ne bombarde pas le serveur d'une requête après chaque lettre, un calculateur de trajectoire avec un historique des résultats et un formulaire d'inscription avec validation. Chacun combine des computed et un watcher, et tout l'art consiste à ne pas confondre les rôles. La règle est courte : quand il te faut une valeur, écris une computed, et quand il te faut une action - une requête, une entrée dans le journal, un minuteur - écris un watch. Le getter d'une computed doit rester pur : il calcule et retourne un résultat, sans envoyer de requêtes, sans écrire dans localStorage et sans modifier d'autres données.

Système de recherche de modules avec Debounce

Un opérateur qui tape le mot "réacteur" enverrait huit requêtes, alors que seule la dernière l'intéresse. La technique du debounce retient l'action tant que les données n'ont pas cessé de changer pendant un temps donné, ici 300 millisecondes. Commençons par le template : un champ de recherche, un message de chargement et une liste de résultats.

1<template>
2  <div class="module-search-system" style="background: #0a0e27; color: #00ff88;">
3    <input v-model="searchInput" placeholder="Rechercher des modules..." style="background: #1a1e37; color: #00ff88;" />
4    <p v-if="isLoading" style="color: #00b4d8;">Analyse de la base de données 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>

Le champ est relié à searchInput, le texte brut qui change après chaque frappe. La liste lit results, et le message s'affiche tant que isLoading vaut true. Voici maintenant le script avec le premier watcher, qui retarde la copie du texte dans 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 - retarde la recherche
10let debounceTimeout
11watch(searchInput, (value) => {
12  clearTimeout(debounceTimeout)
13  debounceTimeout = setTimeout(() => {
14    searchQuery.value = value
15  }, 300)
16})

L'ordre des étapes mérite d'être retenu : la variable debounceTimeout en dehors du watcher, puis watch(searchInput, ...), et à l'intérieur clearTimeout, qui annule le compte à rebours précédent, et setTimeout, qui en lance un nouveau. Chaque frappe remet l'horloge à zéro, donc seul un texte suivi d'une pause arrive dans searchQuery. Le champ à l'écran affiche toujours les lettres tout de suite : seule la recherche attend. Le second watcher lance la vraie requête.

1// Exécute la recherche
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>

Le callback est asynchrone, et le bloc try/finally garantit que isLoading repasse à false même après une erreur réseau. L'asynchronisme a pourtant un prix : si une ancienne requête revient après une plus récente, elle écrase les résultats frais. Dans un panneau de production, ajoute donc ici un nettoyage avec onCleanup et AbortController, comme dans la leçon sur l'arrêt des watchers.

Calculateur de trajectoire avec historique

Le deuxième panneau calcule une opération sur la vitesse et le temps et tient un historique des résultats. Le résultat est une valeur, il va donc dans une computed, et une entrée d'historique est une action, elle va donc dans un watcher. Le template contient deux champs numériques, une liste avec l'opération et l'historique.

1<template>
2  <div class="trajectory-calc" style="background: #0a0e27; color: #00ff88;">
3    <label>Vitesse (km/s) :</label>
4    <input v-model.number="velocity" type="number" style="background: #1a1e37; color: #00ff88;" />
5
6    <label>Opération :</label>
7    <select v-model="operation" style="background: #1a1e37; color: #00ff88;">
8      <option value="+">Ajouter</option>
9      <option value="-">Soustraire</option>
10      <option value="*">Multiplier</option>
11      <option value="/">Diviser</option>
12    </select>
13
14    <label>Temps (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>Historique des calculs :</h3>
19    <ul>
20      <li v-for="(entry, i) in history" :key="i" style="color: #00b4d8;">{{ entry }}</li>
21    </ul>
22  </div>
23</template>

Les champs utilisent v-model.number, donc ce sont des nombres et non du texte qui arrivent dans les variables, et la liste history affiche ses entrées sous le résultat. La première partie du script contient l'état du panneau et la computed du résultat.

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 : 'Erreur'
15    default: return 0
16  }
17})

L'instruction switch choisit l'opération, et en cas de division par zéro la computed retourne le texte Erreur au lieu de l'infini. La seconde partie du script ajoute les résultats à l'historique.

1// Enregistre dans l'historique des calculs
2watch(result, (newResult) => {
3  const entry = `${velocity.value} ${operation.value} ${time.value} = ${newResult}`
4  history.value.unshift(entry)
5
6  // Limite à 10 entrées
7  if (history.value.length > 10) {
8    history.value.pop()
9  }
10})
11</script>

Le watcher observe la computed result, car une computed est une source tout à fait valable pour watch. La méthode unshift ajoute une entrée au début du tableau, et pop retire la plus ancienne dès qu'il y a plus de 10 entrées. Note un point moins évident : quand la vitesse vaut 0, modifier le temps en mode multiplication n'ajoute aucune entrée, car le résultat vaut toujours 0 et le watcher ne réagit qu'à un changement de valeur.

Validation de formulaire

Le troisième panneau est une inscription avec une adresse e-mail et un mot de passe. Ici, les computed suffisent, car chaque message et chaque état du bouton est une valeur déduite de ce que l'utilisateur a tapé. Le template relie les champs, affiche les erreurs et bloque le bouton.

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="Mot de passe" />
10      <div class="strength" :class="passwordStrengthClass">
11        Robustesse du mot de passe : {{ passwordStrength }}
12      </div>
13    </div>
14
15    <button :disabled="!isFormValid">S'inscrire</button>
16  </form>
17</template>

Le bouton reçoit :disabled="!isFormValid", il se débloque donc tout seul dès que le formulaire devient valide, et :class change l'apparence de l'indicateur de robustesse du mot de passe. La première partie du script définit les champs et deux computed avec les messages.

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) ? '' : 'Email invalide'
11})
12
13const passwordStrength = computed(() => {
14  const pwd = password.value
15  if (pwd.length < 6) return 'Faible'
16  if (pwd.length < 10) return 'Moyen'
17  if (/[A-Z]/.test(pwd) && /[0-9]/.test(pwd)) return 'Fort'
18  return 'Moyen'
19})

emailError vérifie d'abord si le champ est vide, car un formulaire vide ne doit pas crier à l'erreur avant que l'utilisateur ait commencé à taper. Ensuite, une expression régulière teste si l'adresse a la forme "quelque-chose@quelque-chose.quelque-chose" sans espaces. passwordStrength évalue le mot de passe d'après sa longueur et la présence d'une majuscule et d'un chiffre. La seconde partie du script s'appuie sur ces résultats.

1const passwordStrengthClass = computed(() => ({
2  weak: passwordStrength.value === 'Faible',
3  medium: passwordStrength.value === 'Moyen',
4  strong: passwordStrength.value === 'Fort'
5}))
6
7const isFormValid = computed(() => {
8  return email.value &&
9         !emailError.value &&
10         password.value.length >= 6
11})
12
13function submit() {
14  console.log('Formulaire envoyé')
15}
16</script>

passwordStrengthClass lit passwordStrength, et isFormValid lit emailError : c'est une chaîne de computed, dans laquelle chaque maillon ne se recalcule qu'après le changement du maillon précédent. Le même mécanisme, sous sa forme la plus simple, ressemble à ceci :

1const price = ref(100)
2const tax = computed(() => price.value * 0.23)
3const total = computed(() => price.value + tax.value)

Modifier price invalide les deux computed, et total se recalcule avec la nouvelle taxe à sa prochaine lecture. La chaîne n'a besoin d'aucun watcher ni d'aucune synchronisation manuelle.

Déboguer les computed et les watchers

Quand une computed ne se recalcule pas au moment où tu l'attends, commence par Vue DevTools. C'est une extension de navigateur, disponible aussi comme plugin Vite et comme application autonome, qui montre l'arbre des composants et l'état de chacun, y compris les valeurs actuelles des ref et des computed. Pour un suivi précis, il existe les options onTrack et onTrigger, passées en second argument de computed ou parmi les options de 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 se déclenche quand le getter enregistre une dépendance, et onTrigger quand un changement de cette dépendance invalide le résultat, à condition que la computed soit réellement utilisée, par exemple dans un template. Ces deux options ne fonctionnent qu'en mode développement, elles ne coûtent donc rien dans la version de production. Je te recommande de commencer par DevTools et de ne sortir onTrack et onTrigger que lorsque l'inspection de l'état ne suffit plus. Dans le projet du module, tu réuniras tous ces outils dans un seul tableau de bord de mission.

Souviens-toi : une computed répond à la question "combien ça vaut", et un watch à la question "que faire quand ça change" - confonds ces rôles, et le panneau de mission calcule trop ou ne réagit pas du tout.

Code de cette leçon : App.vue
1<script setup>
2import { ref, watch, watchEffect, onUnmounted } from 'vue'
3
4// NOVA LAB - Observation avancée : sources multiples et 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 sur plusieurs 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  // Calcule la moyenne
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: 'Moyenne sous le seuil !',
54      color: '#ff0044'
55    })
56  }
57})
58
59// Watch avec fonction de cleanup
60watch(isMonitoring, (newValue, oldValue, onCleanup) => {
61  const timestamp = new Date().toLocaleTimeString()
62
63  if (newValue) {
64    alerts.value.unshift({
65      time: timestamp,
66      message: '▶Surveillance continue démarrée',
67      color: '#00ff88'
68    })
69
70    // Démarre l'intervalle
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      // Simule les fluctuations des capteurs
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    // Fonction de cleanup - appelée quand le watcher s'arrête ou avant une nouvelle exécution
88    onCleanup(() => {
89      if (monitoringInterval) {
90        clearInterval(monitoringInterval)
91        const stopTime = new Date().toLocaleTimeString()
92        alerts.value.unshift({
93          time: stopTime,
94          message: '⏸Surveillance arrêtée (cleanup exécuté)',
95          color: '#ffaa00'
96        })
97      }
98    })
99  }
100})
101
102// watchEffect pour la moyenne calculée
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 au démontage
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">Sources multiples et cleanup de watch</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 ? '⏸Arrêter la surveillance' : '▶Démarrer la surveillance' }}
155      </button>
156      <div class="status" :class="{ monitoring: isMonitoring }">
157        {{ isMonitoring ? 'ACTIF' : 'INACTIF' }}
158      </div>
159    </div>
160
161    <div class="sensors-grid">
162      <div class="sensor-card sensor-a">
163        <h3>Capteur 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>Capteur 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>Capteur 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>Moyenne</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">Réinitialiser les capteurs</button>
220      <button @click="clearAlerts">Effacer les logs</button>
221    </div>
222
223    <div class="data-panels">
224      <div class="panel alerts-panel">
225        <h3>Journal des alertes</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">Capteur {{ 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            Aucune alerte pour l'instant...
242          </div>
243        </div>
244      </div>
245
246      <div class="panel readings-panel">
247        <h3>Relevés des capteurs (capture auto)</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              Moy: {{ reading.avg }}%
260            </span>
261          </div>
262          <div v-if="sensorReadings.length === 0" class="no-data">
263            Démarre la surveillance pour capturer des relevés...
264          </div>
265        </div>
266      </div>
267    </div>
268
269    <div class="info-box">
270      <strong>Observation avancée :</strong> observe plusieurs sources avec <code>watch([a, b, c], ...)</code>.
271      Utilise <code>onCleanup()</code> pour nettoyer les effets de bord comme les intervalles quand le watcher s'arrête !
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>

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. Quand vaut-il mieux utiliser computed plutôt que watch ?

  2. 2. Quand vaut-il mieux utiliser watch plutôt que computed ?

Voici 2 des 10 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Complète la recherche avec debounce en utilisant watch pour retarder l'envoi de la requête.

  • Clique dans l'ordre

    Ordonne l'import correct de computed et watch depuis Vue :

  • Tri vertical

    Ordonne les modes de flush de watch, de l'exécution la plus précoce à la plus tardive :

  • Tri horizontal

    Ordonne le watch qui observe une propriété d'un objet réactif :

  • Clique dans l'ordre

    Ordonne le watch qui observe plusieurs refs simultanément :

  • Tri vertical

    Ordonne les étapes de création d'une propriété calculée du début à la fin :

  • Tri horizontal

    Ordonne la destructuration correcte dans un watch sur plusieurs sources :

  • Clique dans l'ordre

    Ordonne watchEffect avec onCleanup dans le bon ordre :

  • Tri horizontal

    Ordonne la computed de validation d'email dans le bon ordre :

  • Tri vertical

    Ordonne les étapes d'implémentation du debounce avec watch du début à la fin :

  • Clique dans l'ordre

    Ordonne la chaîne de dépendances des computed, de la source à la valeur finale :

  • Tri horizontal

    Ordonne l'appel correct de watchPostEffect :