💻 Exemplos: Capítulo 04: Propriedades Computadas (computed) e Observadores (watch, watchEffect)
Especialização em Front-End com Vue • Demonstrações Práticas Compiláveis
💻 Código de Exemplo 04 — Otimização de Derivações com computed e Efeitos com watch / watchEffect
O exemplo abaixo demonstra a distinção fundamental entre cálculos puros com cache (computed, incluindo suporte a getter/setter bidirecional) e efeitos colaterais assíncronos (watch e watchEffect) com limpeza de recursos (onCleanup).
<!-- src/components/DataAnalyticsWatcher.vue -->
<script setup lang="ts">
import { ref, computed, watch, watchEffect } from 'vue';
// Estado de entrada de dados
const baseSalary = ref<number>(5000);
const performanceScore = ref<number>(85); // 0 a 100
const searchKeyword = ref<string>('');
const logs = ref<string[]>([]);
// 1. COMPUTED PURA (Leitura com Caching Inteligente)
const bonusPercentage = computed<number>(() => {
if (performanceScore.value >= 90) return 0.25;
if (performanceScore.value >= 75) return 0.15;
if (performanceScore.value >= 50) return 0.05;
return 0.0;
});
// 2. COMPUTED BIDIRECIONAL (Getter e Setter)
const totalCompensation = computed<number>({
get: () => {
return baseSalary.value * (1 + bonusPercentage.value);
},
set: (newTotal: number) => {
// Ajusta o salário-base inversamente
baseSalary.value = Math.round(newTotal / (1 + bonusPercentage.value));
}
});
// 3. WATCH ESPECÍFICO (Com valor antigo, novo e opção immediate)
watch(
performanceScore,
(newScore, oldScore) => {
const entry = `[Score Watcher] Alteração de ${oldScore ?? 0} para ${newScore} pontos.`;
logs.value.unshift(entry);
},
{ immediate: false }
);
// 4. WATCH COM DEBOUNCE E CLEANUP (Efeito Colateral Assíncrono)
watch(searchKeyword, (term, _, onCleanup) => {
if (!term.trim()) return;
const timer = setTimeout(() => {
logs.value.unshift(`[API Search] Consulta disparada para termo: "${term}"`);
}, 500);
// onCleanup cancela o timer anterior se o usuário continuar digitando
onCleanup(() => {
clearTimeout(timer);
});
});
// 5. WATCHEFFECT (Rastreamento Automático de Dependências)
watchEffect((onCleanup) => {
const currentTotal = totalCompensation.value;
const currentBase = baseSalary.value;
const message = `[Audit Sync] Folha recalculada: Base R$ ${currentBase} -> Total R$ ${currentTotal}`;
// Simulação de persistência em log
console.log(message);
onCleanup(() => {
// Executado imediatamente antes do re-efeito ou desmontagem
});
});
function adjustTotalManual(): void {
// Dispara o setter de totalCompensation
totalCompensation.value = 8000;
}
</script>
<template>
<div class="max-w-2xl mx-auto p-6 bg-white border rounded-2xl shadow-sm space-y-6">
<h2 class="text-xl font-bold text-slate-800 border-b pb-3">
📊 Cálculo Reativo e Efeitos Colaterais no Vue 3
</h2>
<!-- Seção de Parâmetros -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-1">
<label class="text-xs font-semibold text-slate-600">Salário Base (R$):</label>
<input
type="number"
v-model.number="baseSalary"
class="w-full border px-3 py-2 rounded-lg text-sm"
/>
</div>
<div class="space-y-1">
<label class="text-xs font-semibold text-slate-600">Score de Performance (0-100):</label>
<input
type="range"
min="0"
max="100"
v-model.number="performanceScore"
class="w-full h-2 bg-slate-200 rounded-lg cursor-pointer"
/>
<div class="text-right text-xs text-slate-500 font-bold">{{ performanceScore }} pts</div>
</div>
</div>
<!-- Resultados Computados -->
<div class="p-4 bg-slate-50 rounded-xl border space-y-2">
<div class="flex justify-between text-sm">
<span class="text-slate-600">Bônus Calculado (Computed):</span>
<span class="font-bold text-emerald-600">+{{ (bonusPercentage * 100).toFixed(0) }}%</span>
</div>
<div class="flex justify-between text-base font-bold border-t pt-2">
<span class="text-slate-800">Remuneração Total:</span>
<span class="text-indigo-600 text-lg">R$ {{ totalCompensation.toFixed(2) }}</span>
</div>
<button
@click="adjustTotalManual"
class="mt-2 text-xs bg-indigo-50 text-indigo-700 px-3 py-1.5 rounded hover:bg-indigo-100"
>
Forçar Total para R$ 8.000,00 (Testar Writable Computed)
</button>
</div>
<!-- Campo de Busca Assíncrona com Debounce Watcher -->
<div class="space-y-1">
<label class="text-xs font-semibold text-slate-600">Pesquisa Assíncrona (Debounced Watch):</label>
<input
type="text"
v-model="searchKeyword"
placeholder="Digite algo para observar o debounce..."
class="w-full border px-3 py-2 rounded-lg text-sm"
/>
</div>
<!-- Painel de Logs de Auditoria -->
<div class="space-y-2">
<h4 class="text-xs font-bold uppercase text-slate-500 tracking-wider">Histórico de Watchers:</h4>
<div class="bg-slate-900 text-emerald-400 p-3 rounded-lg font-mono text-xs max-h-36 overflow-y-auto space-y-1">
<p v-if="logs.length === 0" class="text-slate-500">// Nenhum evento disparado ainda...</p>
<p v-for="(log, idx) in logs" :key="idx">{{ log }}</p>
</div>
</div>
</div>
</template>
🖥️ Saída / Comportamento Esperado
Renderização Inicial:
Salário Base: R$ 5.000,00
Performance: 85 pts -> Bônus: +15%
Remuneração Total (Computed): R$ 5.750,00
Console log: "[Audit Sync] Folha recalculada: Base R$ 5000 -> Total R$ 5750"
Interação 1 — Alterar slider de Performance para 95:
Bônus passa automaticamente para +25%.
Remuneração Total atualiza para R$ 6.250,00.
Log adicionado: "[Score Watcher] Alteração de 85 para 95 pontos."
Interação 2 — Clicar em "Forçar Total para R$ 8.000,00":
O setter de totalCompensation calcula o novo salário-base: 8000 / 1.25 = R$ 6.400,00.
baseSalary.value é atualizado para 6400 reativamente.
Interação 3 — Digitar rapidamente "vue" na pesquisa:
O debounce aguarda 500ms de inatividade.
Log adicionado: "[API Search] Consulta disparada para termo: 'vue'".
🔍 Destaques da Implementação
- Computed Caching:
bonusPercentagesó recalcula seperformanceScore.valuesofrer alteração, retornando o valor em cache em leituras repetidas. - Writable Computed: O padrão
get/setviabiliza manipulação bidirecional de dados sem violar a regra de responsabilidade única. onCleanupAnti-Race-Condition: Garante cancelamento de timeouts e abortamento de requisições de rede ao digitar novos caracteres.