Cours Vue.js · Module 3: Réactivité
La réactivité en pratique - Le panneau de contrôle
Dans cette leçon5
Combinons tout dans des exemples pratiques de systèmes du NOVA LAB.
Jusqu'ici, tu as observé chaque outil de réactivité séparément. Les vrais panneaux de la station les combinent tous à la fois : le formulaire de configuration doit valider les données à la volée, le moniteur des modules filtre la liste selon la phrase saisie, et le système d'approvisionnement calcule les totaux de la commande. Les trois panneaux ci-dessous partagent la même organisation en couches. D'abord les données dans ref() ou reactive(), ensuite les valeurs dérivées avec computed(), puis les actions, c'est-à-dire les fonctions qui modifient l'état, et enfin le template avec des liaisons réactives.
Panneau de configuration du réacteur avec validation
Le template du formulaire utilise v-model, la liaison bidirectionnelle entre un champ et les données, que tu découvriras en détail dans le module 5. Le modificateur .number transforme le texte saisi en nombre, et tu connais @submit.prevent et :disabled depuis le module 2 :
1<template>
2 <form @submit.prevent="submitConfig" class="reactor-config" style="background: #0a0e27; color: #00ff88;">
3 <div>
4 <label>Nom du module :</label>
5 <input v-model="config.name" style="background: #1a1e37; color: #00ff88;" />
6 <span v-if="errors.name" style="color: #ff4444;">{{ errors.name }}</span>
7 </div>
8
9 <div>
10 <label>Température maximale (K) :</label>
11 <input v-model.number="config.maxTemp" type="number" style="background: #1a1e37; color: #00ff88;" />
12 <span v-if="errors.maxTemp" style="color: #ff4444;">{{ errors.maxTemp }}</span>
13 </div>
14
15 <div>
16 <label>Statut :</label>
17 <input v-model="config.status" style="background: #1a1e37; color: #00ff88;" />
18 </div>
19
20 <button :disabled="!isValid" style="background: #00b4d8; color: #0a0e27;">Enregistrer la configuration</button>
21 </form>
22</template>Les messages d'erreur apparaissent via v-if seulement quand il y a quelque chose à montrer, et le bouton d'enregistrement reste bloqué tant que le formulaire n'est pas valide. Les données du formulaire forment un groupe cohérent de champs, donc nous les gardons dans reactive() :
1<script setup>
2import { reactive, computed } from 'vue'
3
4const config = reactive({
5 name: '',
6 maxTemp: null,
7 status: ''
8})La validation est une valeur dérivée de ces champs. Dans la première version de ce panneau, la propriété computed écrivait elle-même les messages dans un objet errors séparé. La documentation de Vue interdit pourtant les effets de bord dans les computed : un getter doit seulement calculer et renvoyer un résultat. C'est pourquoi les erreurs elles-mêmes sont ici des computed :
1// La validation comme valeur dérivée - sans effets de bord
2const errors = computed(() => ({
3 // Validation du nom
4 name: config.name.length < 3 ? 'Le nom doit comporter au moins 3 caractères' : '',
5 // Validation de la température
6 maxTemp: config.maxTemp && (config.maxTemp < 1000 || config.maxTemp > 10000)
7 ? 'La température doit être comprise entre 1000 et 10000 K'
8 : ''
9}))
10
11const isValid = computed(() => !errors.value.name && !errors.value.maxTemp)Le template n'a demandé aucun changement : errors est un ref de niveau supérieur, donc errors.name y fonctionne sans .value. La saisie du troisième caractère du nom recalcule les deux computed et débloque le bouton. L'action qui envoie la configuration ne lit plus que le résultat final :
1function submitConfig() {
2 if (isValid.value) {
3 console.log('Envoi de la configuration au NOVA LAB :', config)
4 }
5}
6</script>Système de surveillance des modules
Le moniteur combine un champ de recherche et une liste déroulante avec une liste filtrée de modules. v-model fonctionne aussi sur <select> :
1<template>
2 <div class="module-monitor" style="background: #0a0e27; color: #00ff88;">
3 <input v-model="searchQuery" placeholder="Rechercher un module..." style="background: #1a1e37; color: #00ff88;" />
4 <select v-model="filterStatus" style="background: #1a1e37; color: #00ff88;">
5 <option value="">Tous les statuts</option>
6 <option value="ACTIVE">Actif</option>
7 <option value="STANDBY">En veille</option>
8 </select>
9
10 <ul>
11 <li v-for="module in filteredModules" :key="module.id" style="border-bottom: 1px solid #00b4d8;">
12 {{ module.name }} - {{ module.status }}
13 </li>
14 </ul>
15
16 <p>Trouvés : {{ filteredModules.length }} modules</p>
17 </div>
18</template>Le template ne filtre rien lui-même - il reçoit la liste prête filteredModules. L'état se compose de deux refs simples et d'un tableau de modules :
1<script setup>
2import { ref, computed } from 'vue'
3
4const searchQuery = ref('')
5const filterStatus = ref('')
6
7const modules = ref([
8 { id: 1, name: 'Oxygen Generator Alpha', status: 'ACTIVE' },
9 { id: 2, name: 'Plasma Core Beta', status: 'STANDBY' },
10 { id: 3, name: 'Water Recycler Gamma', status: 'ACTIVE' },
11 { id: 4, name: 'Power Distributor Delta', status: 'STANDBY' }
12])Le filtre combine les deux conditions. L'écriture !filterStatus.value || signifie : quand aucun statut n'est choisi, laisse passer chaque module :
1const filteredModules = computed(() => {
2 return modules.value.filter(module => {
3 const matchesSearch = module.name
4 .toLowerCase()
5 .includes(searchQuery.value.toLowerCase())
6
7 const matchesStatus = !filterStatus.value ||
8 module.status === filterStatus.value
9
10 return matchesSearch && matchesStatus
11 })
12})
13</script>Le tableau original modules reste intact, et la liste à l'écran se rafraîchit à chaque caractère saisi.
Système d'approvisionnement de la mission
La commande d'approvisionnement affiche le nombre d'articles, les lignes avec un bouton de suppression et le total en crédits :
1<template>
2 <div class="supply-cart" style="background: #0a0e27; color: #00ff88;">
3 <h2>Commande ({{ supplies.itemCount }} articles)</h2>
4
5 <div v-for="item in supplies.items" :key="item.id" style="border-bottom: 1px solid #00b4d8;">
6 {{ item.name }} - {{ item.quantity }} x {{ item.price }} CR
7 <button @click="removeItem(item.id)" style="background: #ff4444; color: #fff;">Retirer</button>
8 </div>
9
10 <p><strong>Total : {{ supplies.total }} CR (Crédits)</strong></p>
11 </div>
12</template>Ici, les valeurs dérivées sont écrites autrement que dans les panneaux précédents : sous forme de getters à l'intérieur même de l'objet réactif. Un getter est une propriété JavaScript dont la valeur est calculée à chaque lecture par une fonction marquée du mot-clé get :
1<script setup>
2import { reactive } from 'vue'
3
4const supplies = reactive({
5 items: [
6 { id: 1, name: 'Oxygen Canister', quantity: 10, price: 50 },
7 { id: 2, name: 'Plasma Cell', quantity: 5, price: 200 }
8 ],
9
10 // Calculé sous forme de propriété
11 get itemCount() {
12 return this.items.reduce((sum, item) => sum + item.quantity, 0)
13 },
14
15 get total() {
16 return this.items.reduce((sum, item) =>
17 sum + item.quantity * item.price, 0)
18 }
19})Au départ, itemCount vaut 15 et total 1500 CR. Un getter calcule son résultat à chaque lecture et, contrairement à computed(), ne le met pas en cache, ce qui n'a pas d'importance pour une liste courte. L'action de suppression modifie le tableau sur place :
1function removeItem(id) {
2 const index = supplies.items.findIndex(item => item.id === id)
3 if (index > -1) {
4 supplies.items.splice(index, 1)
5 }
6}
7</script>Après le retrait des cellules à plasma, le total tombe à 500 CR, car splice est une méthode que Vue détecte.
readonly() - Un affichage sans droit d'écriture
Le moniteur du mess doit afficher l'état du réacteur, mais il ne doit pas pouvoir le modifier. readonly() renvoie un Proxy profond en lecture seule :
1import { reactive, readonly } from 'vue'
2
3const reactorState = reactive({ temperature: 2500 })
4const reactorView = readonly(reactorState)
5
6reactorView.temperature = 9999 // avertissement dans la console, changement refusé
7reactorState.temperature = 2600 // un changement via l'original fonctionne
8console.log(reactorView.temperature) // 2600En mode développement, une tentative d'écriture se termine par un avertissement de Vue, et la valeur reste inchangée. La vue en lecture seule n'est pas une copie : elle montre chaque changement de l'original. Le modèle est simple - tu gardes l'état modifiable pour toi, tu partages la version readonly avec l'extérieur, et tu ne fais les changements que par tes propres fonctions.
Choisir le bon outil
La documentation de Vue recommande ref() comme outil principal. reactive() convient aux groupes cohérents de champs, shallowRef() avec triggerRef() aux énormes listes, markRaw() aux instances de bibliothèques externes, et readonly() au partage de l'état. Sous le capot, Vue 3 intercepte l'accès aux objets avec le Proxy natif, et l'accès aux refs avec des getters et des setters. Tiens-toi à la séparation entre données, valeurs dérivées, actions et template et chaque panneau restera lisible. Dans le projet du module, tu construiras à partir de ces couches un système complet de surveillance du réacteur.
Retiens : un bon panneau de station a une seule source de vérité, des valeurs dérivées au lieu de copies manuelles, et des actions qui sont les seules à toucher l'état.
Code de cette leçon : App.vue
1<script setup>
2import { ref, reactive, markRaw, isReactive, toRaw } from 'vue'
3
4// markRaw() - Contrôle du réacteur à plasma
5// Contrôle avancé de la réactivité au NOVA LAB
6
7// Simulateur de gros objet (ex. bibliothèque 3D)
8class HeavySimulationEngine {
9 constructor(name) {
10 this.name = name
11 this.iterations = 0
12 this.data = new Array(1000).fill(0).map((_, i) => ({
13 id: i,
14 value: Math.random() * 100
15 }))
16 }
17
18 simulate() {
19 this.iterations++
20 // Calculs lourds
21 return this.data.reduce((sum, item) => sum + item.value, 0)
22 }
23
24 reset() {
25 this.iterations = 0
26 }
27}
28
29// Réacteur avec objet normal (réactif)
30const reactiveReactor = reactive({
31 id: 'REACTOR-A',
32 status: 'ONLINE',
33 power: 100,
34 engine: new HeavySimulationEngine('Reactive Engine'),
35 lastUpdate: new Date().toISOString()
36})
37
38// Réacteur avec markRaw (moteur non réactif)
39const optimizedReactor = reactive({
40 id: 'REACTOR-B',
41 status: 'ONLINE',
42 power: 100,
43 engine: markRaw(new HeavySimulationEngine('Optimized Engine')),
44 lastUpdate: new Date().toISOString()
45})
46
47// Vérification de la réactivité
48const reactiveEngineIsReactive = isReactive(reactiveReactor.engine)
49const optimizedEngineIsReactive = isReactive(optimizedReactor.engine)
50
51// Compteurs pour la démonstration des performances
52const reactiveSimulationCount = ref(0)
53const optimizedSimulationCount = ref(0)
54
55function runReactiveSimulation() {
56 const start = performance.now()
57
58 // Modification du moteur réactif
59 reactiveReactor.engine.simulate()
60 reactiveReactor.lastUpdate = new Date().toISOString()
61 reactiveSimulationCount.value++
62
63 const end = performance.now()
64 console.log(`Reactive simulation: ${(end - start).toFixed(2)}ms`)
65}
66
67function runOptimizedSimulation() {
68 const start = performance.now()
69
70 // Modification du moteur markRaw
71 optimizedReactor.engine.simulate()
72 optimizedReactor.lastUpdate = new Date().toISOString()
73 optimizedSimulationCount.value++
74
75 const end = performance.now()
76 console.log(`Optimized simulation: ${(end - start).toFixed(2)}ms`)
77}
78
79// Exemple pratique - map avec beaucoup d'objets
80const sensorNetwork = reactive({
81 location: 'Mars Base Alpha',
82 activeSensors: 0,
83 sensors: markRaw(new Map([
84 ['TEMP-001', { type: 'temperature', value: 22 }],
85 ['PRES-001', { type: 'pressure', value: 101 }],
86 ['OXY-001', { type: 'oxygen', value: 21 }]
87 ]))
88})
89
90function addSensor() {
91 const id = `SENS-${Date.now()}`
92 sensorNetwork.sensors.set(id, {
93 type: 'generic',
94 value: Math.random() * 100
95 })
96 // On met à jour le compteur réactif
97 sensorNetwork.activeSensors = sensorNetwork.sensors.size
98}
99
100function removeSensor() {
101 const keys = Array.from(sensorNetwork.sensors.keys())
102 if (keys.length > 0) {
103 sensorNetwork.sensors.delete(keys[keys.length - 1])
104 sensorNetwork.activeSensors = sensorNetwork.sensors.size
105 }
106}
107
108// toRaw() - accès à l'objet original
109function getRawReactor() {
110 const raw = toRaw(reactiveReactor)
111 console.log('Raw reactor (non-reactive):', raw)
112 console.log('Is reactive?', isReactive(raw)) // false
113}
114
115// Exemple d'intégration d'une bibliothèque externe
116const chartConfig = reactive({
117 title: 'Reactor Power Output',
118 // Grosse bibliothèque de graphiques - on n'a pas besoin qu'elle soit réactive
119 chartInstance: markRaw({
120 render() {
121 return 'Chart rendered'
122 },
123 update(data) {
124 console.log('Chart updated with:', data)
125 },
126 destroy() {
127 console.log('Chart destroyed')
128 }
129 })
130})
131
132function updateChart() {
133 chartConfig.chartInstance.update({
134 power: reactiveReactor.power,
135 timestamp: new Date()
136 })
137}
138</script>
139
140<template>
141 <div class="mark-raw-demo">
142 <div class="header">
143 <h1>CONTRÔLE DU RÉACTEUR - NOVA LAB</h1>
144 <p class="subtitle">markRaw() et toRaw() - Contrôle avancé de la réactivité</p>
145 </div>
146
147 <!-- Comparaison des performances -->
148 <div class="comparison-section">
149 <h2>Comparaison des performances : Reactive vs markRaw</h2>
150
151 <div class="reactors-grid">
152 <!-- Réacteur A - Reactive -->
153 <div class="reactor-card">
154 <div class="reactor-header">
155 <h3>{{ reactiveReactor.id }}</h3>
156 <span class="badge badge-reactive">REACTIVE</span>
157 </div>
158
159 <div class="reactor-stats">
160 <div class="stat">
161 <div class="label">Status</div>
162 <div class="value">{{ reactiveReactor.status }}</div>
163 </div>
164 <div class="stat">
165 <div class="label">Puissance</div>
166 <div class="value">{{ reactiveReactor.power }}%</div>
167 </div>
168 <div class="stat">
169 <div class="label">Itérations du moteur</div>
170 <div class="value">{{ reactiveReactor.engine.iterations }}</div>
171 </div>
172 <div class="stat">
173 <div class="label">Moteur réactif ?</div>
174 <div class="value">{{ reactiveEngineIsReactive ? 'OUI' : 'NON' }}</div>
175 </div>
176 </div>
177
178 <div class="simulation-info">
179 <p>Simulations lancées : <strong>{{ reactiveSimulationCount }}</strong></p>
180 <p class="warning">Chaque changement dans le moteur déclenche la réactivité !</p>
181 </div>
182
183 <button @click="runReactiveSimulation" class="btn btn-primary">
184 ▶Lancer la simulation
185 </button>
186
187 <div class="code-box">
188 <pre>// Objet normal - réactif
189const reactor = reactive({
190 engine: new Engine()
191})
192// engine et toutes ses propriétés
193// sont réactifs Plus lent !</pre>
194 </div>
195 </div>
196
197 <!-- Reactor B - Optimized -->
198 <div class="reactor-card">
199 <div class="reactor-header">
200 <h3>{{ optimizedReactor.id }}</h3>
201 <span class="badge badge-optimized">OPTIMIZED</span>
202 </div>
203
204 <div class="reactor-stats">
205 <div class="stat">
206 <div class="label">Status</div>
207 <div class="value">{{ optimizedReactor.status }}</div>
208 </div>
209 <div class="stat">
210 <div class="label">Puissance</div>
211 <div class="value">{{ optimizedReactor.power }}%</div>
212 </div>
213 <div class="stat">
214 <div class="label">Itérations du moteur</div>
215 <div class="value">{{ optimizedReactor.engine.iterations }}</div>
216 </div>
217 <div class="stat">
218 <div class="label">Moteur réactif ?</div>
219 <div class="value">{{ optimizedEngineIsReactive ? 'OUI' : 'NON' }}</div>
220 </div>
221 </div>
222
223 <div class="simulation-info">
224 <p>Simulations lancées : <strong>{{ optimizedSimulationCount }}</strong></p>
225 <p class="success">Le moteur n'est pas réactif - plus performant !</p>
226 </div>
227
228 <button @click="runOptimizedSimulation" class="btn btn-success">
229 Lancer la simulation (plus rapide)
230 </button>
231
232 <div class="code-box">
233 <pre>// markRaw - non réactif
234const reactor = reactive({
235 engine: markRaw(new Engine())
236})
237// engine N'EST PAS réactif
238// Plus rapide !</pre>
239 </div>
240 </div>
241 </div>
242 </div>
243
244 <!-- Sensor network -->
245 <div class="sensors-section">
246 <h2>Réseau de capteurs (exemple pratique de markRaw)</h2>
247
248 <div class="sensors-card">
249 <div class="sensors-header">
250 <h3>{{ sensorNetwork.location }}</h3>
251 <div class="sensor-count">
252 Capteurs actifs : <strong>{{ sensorNetwork.activeSensors }}</strong>
253 </div>
254 </div>
255
256 <div class="sensors-grid">
257 <div
258 v-for="[id, sensor] in sensorNetwork.sensors"
259 :key="id"
260 class="sensor-item"
261 >
262 <div class="sensor-id">{{ id }}</div>
263 <div class="sensor-type">{{ sensor.type }}</div>
264 <div class="sensor-value">{{ sensor.value.toFixed(2) }}</div>
265 </div>
266 </div>
267
268 <div class="controls">
269 <button @click="addSensor" class="btn btn-success">
270 Ajouter un capteur
271 </button>
272 <button @click="removeSensor" class="btn btn-danger">
273 Retirer un capteur
274 </button>
275 </div>
276
277 <div class="info">
278 <p>La map des capteurs utilise <code>markRaw()</code></p>
279 <p>Seul le compteur <code>activeSensors</code> est réactif</p>
280 </div>
281
282 <div class="code-box">
283 <pre>const network = reactive({
284 activeSensors: 0,
285 // La Map n'a pas besoin d'être réactive
286 sensors: markRaw(new Map())
287})
288
289// Ne mets à jour que le compteur réactif
290network.activeSensors = network.sensors.size</pre>
291 </div>
292 </div>
293 </div>
294
295 <!-- Intégration de bibliothèques -->
296 <div class="library-section">
297 <h2>Intégration avec des bibliothèques externes</h2>
298
299 <div class="library-card">
300 <h3>{{ chartConfig.title }}</h3>
301
302 <div class="chart-placeholder">
303 <div class="chart-visual">
304 Graphique de la puissance du réacteur
305 </div>
306 <p>Instance du graphique : {{ chartConfig.chartInstance.render() }}</p>
307 </div>
308
309 <button @click="updateChart" class="btn btn-info">
310 Mettre à jour le graphique
311 </button>
312
313 <div class="code-box">
314 <pre>import Chart from 'chart-library'
315
316const config = reactive({
317 title: 'My Chart',
318 // Grosse bibliothèque - on n'a pas besoin de réactivité
319 chartInstance: markRaw(new Chart())
320})
321
322// Mise à jour manuelle quand nécessaire
323config.chartInstance.update(newData)</pre>
324 </div>
325 </div>
326 </div>
327
328 <!-- toRaw() demo -->
329 <div class="toraw-section">
330 <h2>toRaw() - Accès à l'objet original</h2>
331
332 <div class="toraw-card">
333 <p>Parfois, tu as besoin d'accéder à l'objet original, non réactif</p>
334
335 <button @click="getRawReactor" class="btn btn-warning">
336 Obtenir le réacteur RAW (regarde la console)
337 </button>
338
339 <div class="code-box">
340 <pre>const reactive = reactive({ value: 1 })
341
342// Récupère l'objet original
343const raw = toRaw(reactive)
344
345// Les changements dans raw ne déclencheront PAS de réactivité
346raw.value = 2
347
348console.log(isReactive(raw)) // false</pre>
349 </div>
350
351 <div class="use-cases">
352 <h4>Quand utiliser toRaw() ?</h4>
353 <ul>
354 <li>Passer à une API qui n'a pas besoin de réactivité</li>
355 <li>Opérations sur des données brutes sans déclencher la réactivité</li>
356 <li>Débogage - vérifier les valeurs originales</li>
357 </ul>
358 </div>
359 </div>
360 </div>
361
362 <!-- Info box -->
363 <div class="info-box">
364 <h3>Quand utiliser markRaw() ?</h3>
365 <div class="use-cases-grid">
366 <div class="use-case">
367 <h4>Gros objets de données</h4>
368 <p>Structures de données gigantesques qui n'ont pas besoin d'être réactives</p>
369 </div>
370 <div class="use-case">
371 <h4>Bibliothèques externes</h4>
372 <p>Instances de Chart.js, Three.js, D3.js - pas besoin de réactivité</p>
373 </div>
374 <div class="use-case">
375 <h4>Map, Set, WeakMap</h4>
376 <p>Collections que tu changes souvent, mais dont tu n'as pas besoin de suivre chaque changement</p>
377 </div>
378 <div class="use-case">
379 <h4>Optimisation des performances</h4>
380 <p>Quand la réactivité ralentit l'application inutilement</p>
381 </div>
382 </div>
383
384 <div class="warning-box">
385 <h4>Attention !</h4>
386 <p><code>markRaw()</code> est irréversible - l'objet ne deviendra jamais réactif !</p>
387 <p>À utiliser uniquement quand tu es sûr que la réactivité n'est pas nécessaire.</p>
388 </div>
389 </div>
390 </div>
391</template>
392
393<style scoped>
394.mark-raw-demo {
395 min-height: 100vh;
396 background: #0a0e27;
397 color: #e0e0e0;
398 padding: 2rem;
399 font-family: 'Courier New', monospace;
400}
401
402.header {
403 text-align: center;
404 margin-bottom: 2rem;
405 border-bottom: 2px solid #00ff88;
406 padding-bottom: 1rem;
407}
408
409.header h1 {
410 color: #00ff88;
411 font-size: 2rem;
412 margin: 0;
413 text-shadow: 0 0 10px #00ff88;
414}
415
416.subtitle {
417 color: #00b4d8;
418 margin: 0.5rem 0 0 0;
419}
420
421.comparison-section,
422.sensors-section,
423.library-section,
424.toraw-section {
425 background: #1a1f3a;
426 padding: 1.5rem;
427 border-radius: 8px;
428 border: 1px solid #00b4d8;
429 margin-bottom: 2rem;
430}
431
432h2 {
433 color: #00ff88;
434 margin-top: 0;
435}
436
437.reactors-grid {
438 display: grid;
439 grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
440 gap: 1.5rem;
441}
442
443.reactor-card,
444.sensors-card,
445.library-card,
446.toraw-card {
447 background: #0a0e27;
448 padding: 1.5rem;
449 border-radius: 6px;
450}
451
452.reactor-header,
453.sensors-header {
454 display: flex;
455 justify-content: space-between;
456 align-items: center;
457 margin-bottom: 1.5rem;
458 padding-bottom: 1rem;
459 border-bottom: 1px solid #333;
460}
461
462.reactor-header h3,
463.sensors-header h3 {
464 color: #00b4d8;
465 margin: 0;
466}
467
468.badge {
469 padding: 0.3rem 0.8rem;
470 border-radius: 20px;
471 font-size: 0.8rem;
472 font-weight: bold;
473}
474
475.badge-reactive {
476 background: #ff8800;
477 color: #0a0e27;
478}
479
480.badge-optimized {
481 background: #00ff88;
482 color: #0a0e27;
483}
484
485.reactor-stats {
486 display: grid;
487 grid-template-columns: repeat(2, 1fr);
488 gap: 1rem;
489 margin-bottom: 1.5rem;
490}
491
492.stat {
493 background: #1a1f3a;
494 padding: 1rem;
495 border-radius: 4px;
496}
497
498.stat .label {
499 font-size: 0.85rem;
500 color: #888;
501 margin-bottom: 0.3rem;
502}
503
504.stat .value {
505 font-size: 1.2rem;
506 color: #00ff88;
507 font-weight: bold;
508}
509
510.simulation-info {
511 background: #1a1f3a;
512 padding: 1rem;
513 border-radius: 4px;
514 margin-bottom: 1rem;
515}
516
517.simulation-info p {
518 margin: 0.5rem 0;
519}
520
521.simulation-info strong {
522 color: #00ff88;
523}
524
525.warning {
526 color: #ff8800;
527}
528
529.success {
530 color: #00ff88;
531}
532
533.btn {
534 width: 100%;
535 padding: 1rem;
536 font-size: 1rem;
537 border: none;
538 border-radius: 6px;
539 cursor: pointer;
540 font-weight: bold;
541 transition: all 0.3s ease;
542 font-family: 'Courier New', monospace;
543 margin-bottom: 1rem;
544}
545
546.btn-primary {
547 background: linear-gradient(135deg, #00b4d8, #0088cc);
548 color: white;
549}
550
551.btn-success {
552 background: linear-gradient(135deg, #00ff88, #00dd77);
553 color: #0a0e27;
554}
555
556.btn-danger {
557 background: linear-gradient(135deg, #ff4444, #cc0000);
558 color: white;
559}
560
561.btn-info {
562 background: linear-gradient(135deg, #00b4d8, #00ff88);
563 color: #0a0e27;
564}
565
566.btn-warning {
567 background: linear-gradient(135deg, #ffaa00, #ff8800);
568 color: #0a0e27;
569}
570
571.btn:hover {
572 transform: scale(1.02);
573 box-shadow: 0 0 20px currentColor;
574}
575
576.code-box {
577 background: #1a1f3a;
578 padding: 1rem;
579 border-radius: 4px;
580 border-left: 3px solid #00ff88;
581}
582
583.code-box pre {
584 margin: 0;
585 color: #00ff88;
586 font-size: 0.85rem;
587 overflow-x: auto;
588}
589
590.sensor-count {
591 color: #00ff88;
592 font-weight: bold;
593}
594
595.sensors-grid {
596 display: grid;
597 grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
598 gap: 1rem;
599 margin: 1.5rem 0;
600}
601
602.sensor-item {
603 background: #1a1f3a;
604 padding: 1rem;
605 border-radius: 4px;
606 border-left: 3px solid #00b4d8;
607}
608
609.sensor-id {
610 color: #00b4d8;
611 font-weight: bold;
612 font-size: 0.9rem;
613}
614
615.sensor-type {
616 color: #888;
617 font-size: 0.85rem;
618 margin: 0.3rem 0;
619 text-transform: uppercase;
620}
621
622.sensor-value {
623 color: #00ff88;
624 font-size: 1.2rem;
625 font-weight: bold;
626}
627
628.controls {
629 display: flex;
630 gap: 1rem;
631}
632
633.controls .btn {
634 flex: 1;
635}
636
637.info {
638 background: #1a1f3a;
639 padding: 1rem;
640 border-radius: 4px;
641 margin-top: 1rem;
642}
643
644.info p {
645 margin: 0.5rem 0;
646 color: #ccc;
647}
648
649.info code {
650 background: #0a0e27;
651 padding: 0.2rem 0.5rem;
652 border-radius: 3px;
653 color: #00ff88;
654}
655
656.chart-placeholder {
657 background: #1a1f3a;
658 padding: 2rem;
659 border-radius: 4px;
660 text-align: center;
661 margin: 1rem 0;
662}
663
664.chart-visual {
665 font-size: 3rem;
666 margin-bottom: 1rem;
667}
668
669.use-cases {
670 background: #1a1f3a;
671 padding: 1rem;
672 border-radius: 4px;
673 margin-top: 1rem;
674}
675
676.use-cases h4 {
677 color: #00b4d8;
678 margin-top: 0;
679}
680
681.use-cases ul {
682 color: #ccc;
683 padding-left: 1.5rem;
684}
685
686.use-cases li {
687 margin: 0.5rem 0;
688}
689
690.info-box {
691 background: #1a1f3a;
692 padding: 1.5rem;
693 border-radius: 8px;
694 border-left: 4px solid #00ff88;
695}
696
697.info-box h3 {
698 color: #00ff88;
699 margin-top: 0;
700}
701
702.use-cases-grid {
703 display: grid;
704 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
705 gap: 1rem;
706 margin: 1rem 0;
707}
708
709.use-case {
710 background: #0a0e27;
711 padding: 1rem;
712 border-radius: 4px;
713}
714
715.use-case h4 {
716 color: #00b4d8;
717 margin-top: 0;
718}
719
720.use-case p {
721 color: #ccc;
722 margin: 0;
723 font-size: 0.9rem;
724}
725
726.warning-box {
727 background: #2a1f1a;
728 border: 2px solid #ff8800;
729 padding: 1rem;
730 border-radius: 4px;
731 margin-top: 1rem;
732}
733
734.warning-box h4 {
735 color: #ffaa00;
736 margin-top: 0;
737}
738
739.warning-box p {
740 color: #ccc;
741 margin: 0.5rem 0;
742}
743
744.warning-box code {
745 background: #0a0e27;
746 padding: 0.2rem 0.5rem;
747 border-radius: 3px;
748 color: #00ff88;
749}
750</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. Quelle est l'approche recommandée pour la réactivité dans Vue 3 ?
2. Quel mécanisme Vue 3 utilise-t-il pour implémenter la réactivité ?
Voici 2 des 11 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Crée un objet réactif et enveloppe-le dans readonly() pour le protéger contre les changements.
- Tri vertical
Range dans le bon ordre les étapes d'utilisation de readonly dans un composant.
- Clique dans l'ordre
Range dans le bon ordre les étapes d'utilisation de markRaw avec une bibliothèque externe.
- Éditeur de code
Utilise markRaw() pour un objet qui n'a pas besoin de réactivité (par exemple une instance de bibliothèque ou une configuration).
- Tri vertical
Range dans le bon ordre les étapes d'optimisation avec triggerRef().
- Clique dans l'ordre
Range dans le bon ordre l'instruction d'import des fonctions réactives.
- Tri vertical
Range les concepts de réactivité du plus simple au plus avancé.
- Tri vertical
Range dans le bon ordre les étapes d'utilisation de shallowRef.
- Tri vertical
Range dans le bon ordre les étapes de conception de l'état dans un composant.
- Clique dans l'ordre
Range dans le bon ordre les étapes de création d'un formulaire réactif.