Cours Vue.js · Module 4: Computed et watch
Computed et Watch en pratique - systèmes de mission
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. Quand vaut-il mieux utiliser computed plutôt que watch ?
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 :