watch y la función de búsqueda
En este capítulo aprenderás a usar watch y watchEffect para observar los cambios de datos, y a implementar el filtrado en tiempo real en el cuadro de búsqueda del blog.
¿En qué escenarios se necesita watch?
En la mayoría de los casos, el enlace reactivo de Vue es suficiente: cuando los datos cambian, la plantilla se actualiza automáticamente.
Pero hay escenarios que requieren queejecutes lógica activamente cuando los datos cambien, por ejemplo:
- Después de escribir en el cuadro de búsqueda, filtrar la lista de artículos
- Cuando el usuario cambie de categoría, actualizar el título de la página
- Después de que los datos cambien, guardarlos en localStorage
- Cuando cambie un campo del formulario, realizar la validación
En estos casos necesitaswatchywatchEffect。
watch — escucha precisa
watchObserva la fuente de datos especificada y ejecuta la función de callback cuando los datos cambien.
Puedes obtenerel nuevo valoryel valor anterior, y hacer una comparación.
Ejemplo
import { ref, watch } from 'vue'
const keyword = ref('')
// watch(fuente de datos observada, función de callback)
watch(keyword, (newVal, oldVal) => {
console.log(`La palabra clave de búsqueda cambió de "${oldVal}" a "${newVal}"`)
// Aquí se ejecuta la lógica de filtrado
})
</script>
<template>
<input v-model="keyword" placeholder="Buscar artículos...">
<p>Búsqueda actual: {{ keyword }}</p>
</template>
Las tres fuentes de datos de watch
| Tipo de fuente de datos | Sintaxis | Ejemplo |
|---|---|---|
| ref único | watch(ref, callback) | watch(keyword, fn) |
| Propiedad de un objeto reactive | watch(() => obj.prop, callback) | watch(() => user.name, fn) |
| Múltiples fuentes de datos | watch([ref1, ref2], callback) | watch([kw, cat], fn) |
Ejemplo
const keyword = ref('')
const category = ref('Todos')
// Observar un solo ref
watch(keyword, (newVal) => {
document.title = newVal ?`Búsqueda: ${newVal}` : 'EJEMPLO Blog'
})
// Observar múltiples fuentes de datos
watch([keyword, category], ([newKw, newCat], [oldKw, oldCat]) => {
console.log('La palabra clave o la categoría cambiaron; vuelve a filtrar los artículos')
// Ejecutar la lógica de filtrado...
})
watch es, por defecto,de ejecución diferida (lazy). Cuando los datos obtienen un valor por primera vez, no se dispara; solo se dispara en cambios posteriores. Si necesitas que se ejecute también al inicializar, añade la
{ immediate: true }opción de configuración.
watchEffect — seguimiento automático de dependencias
watchEffectNo es necesario especificar manualmente la fuente de datos; rastrea automáticamente los datos reactivos utilizados en la función de callback.
La mayor diferencia con watch:watchEffect se ejecuta una vez de inmediato.。
Ejemplo
import { ref, watchEffect } from 'vue'
const keyword = ref('')
const articles = ref([
{ id: 1, title: 'Introducción a Vue3' },
{ id: 2, title: 'JS asíncrono' },
])
const filteredArticles = ref([])
// watchEffect rastrea automáticamente keyword y articles
// y se ejecuta una vez de inmediato para inicializar filteredArticles
watchEffect(() => {
// Todos los datos reactivos utilizados en la función se rastrean automáticamente
filteredArticles.value = articles.value.filter(
a => a.title.includes(keyword.value)
)
console.log(`Resultado del filtrado: ${filteredArticles.value.length} artículos`)
})
</script>
<template>
<input v-model="keyword" placeholder="Buscar">
<div v-for="a in filteredArticles" :key="a.id">
{{ a.title }}
</div>
</template>
Comparación: watch vs watchEffect
| Característica | watch | watchEffect |
|---|---|---|
| ¿Se especifica la fuente de datos? | Especificación manual | Seguimiento automático de dependencias |
| Ejecución inicial | No se ejecuta por defecto (configurable con immediate) | Se ejecuta inmediatamente |
| Obtener el valor anterior | Puede obtener el valor anterior | No puede obtener el valor anterior |
| Casos de uso | Cuando se necesita comparar el valor anterior y el nuevo y controlar el momento con precisión | Cuando no te importan los valores anteriores y solo quieres «reaccionar a los cambios» |
Regla simple: si tu lógica es «cuando X cambie, haz Y», usa watch. Si tu lógica es «siempre que una dependencia cambie, vuelve a calcular el resultado», usa watchEffect o computed. En realidad, los datos que se pueden calcular se deben resolver con computed; watch se usa para manejar «efectos secundarios», como llamar a una API, guardar en local o manipular el DOM.
Concepto de debounce
Cada pulsación de tecla en el cuadro de búsqueda dispara el filtrado; para datos locales esto no es problema, pero si cada entrada requiere una solicitud de red, se genera un gran desperdicio.
El debounce (antirrebote)consiste en esperar a que el usuario deje de escribir durante un tiempo antes de ejecutar la operación.
Ejemplo
const keyword = ref('')
const filteredArticles = ref([])
let timer = null // ID del temporizador
watch(keyword, (newVal) => {
// Cancela el temporizador anterior
clearTimeout(timer)
// Solo ejecuta el filtrado si no hay nuevos cambios de entrada en 300 ms
timer = setTimeout(() => {
filteredArticles.value = articles.value.filter(
a => a.title.includes(newVal) || a.summary.includes(newVal)
)
}, 300)
})
Para este proyecto de blog, los datos están en local, por lo que el debounce no es imprescindible. Pero conocer esta idea es muy útil para futuros proyectos grandes.
Manos a la obra: añade búsqueda en tiempo real al blog
Añade un cuadro de búsqueda al componente NavBar; al escribir una palabra clave, filtra en tiempo real los títulos y resúmenes de los artículos.
Ejemplo
<script setup>
import { ref, computed, onMounted } from 'vue'
import BlogCard from '../components/BlogCard.vue'
import CategoryFilter from '../components/CategoryFilter.vue'
const articles = ref([]) // todos los artículos
const isLoading = ref(true)
const activeCategory = ref('Todos')
const keyword = ref('') // palabra clave de búsqueda
// Extrae todas las categorías
const categories = computed(() => {
const cats = articles.value.map(a => a.category)
return ['Todos', ...new Set(cats)]
})
// computed rastrea automáticamente los cambios de keyword y activeCategory
// Si cualquiera de los dos cambia, el resultado del filtrado se recalcula automáticamente
const filteredArticles = computed(() => {
let result = articles.value
// Primer paso: filtrar por categoría
if (activeCategory.value !== 'Todos') {
result = result.filter(a => a.category === activeCategory.value)
}
// Segundo paso: filtrar por palabra clave (buscar en título y resumen)
if (keyword.value.trim()) {
const kw = keyword.value.trim().toLowerCase()
result = result.filter(a =>
a.title.toLowerCase().includes(kw) ||
a.summary.toLowerCase().includes(kw)
)
}
return result
})
// Al cambiar de categoría no se limpia la búsqueda; ambos filtros funcionan en conjunto
function handleCategoryChange(cat) {
activeCategory.value = cat
}
// Cargar datos
onMounted(async () => {
try {
const res = await fetch('/posts.json')
articles.value = await res.json()
} catch (err) {
console.error('Error al cargar', err)
} finally {
isLoading.value = false
}
})
</script>
<template>
<div class="home">
<!-- Cuadro de búsqueda -->
<div class="search-bar">
<input
v-model="keyword"
type="text"
placeholder="Buscar por título o resumen..."
class="search-input"
/>
<span v-if="keyword" class="clear-btn" @click="keyword = ''">✕</span>
</div>
<!-- Filtro de categorías -->
<CategoryFilter
:categories="categories"
:active-category="activeCategory"
@update-category="handleCategoryChange"
/>
<!-- Cargando / vacío / normal -->
<p v-if="isLoading">Cargando...</p>
<p v-else-if="filteredArticles.length === 0">No se encontraron artículos que coincidan</p>
<div v-else class="article-grid">
<BlogCard
v-for="article in filteredArticles"
:key="article.id"
:id="article.id"
:title="article.title"
:summary="article.summary"
:date="article.date"
:category="article.category"
/>
</div>
</div>
</template>
<style scoped>
.home { max-width: 960px; margin: 0 auto; padding: 20px; }
.search-bar {
position: relative;
margin-bottom: 20px;
}
.search-input {
width: 100%;
padding: 12px 40px 12px 16px;
border: 2px solid #eee;
border-radius: 8px;
font-size: 15px;
outline: none;
transition: border-color 0.2s;
}
.search-input:focus {
border-color: #427DB8;
}
.clear-btn {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #999;
font-size: 18px;
}
.article-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
margin-top: 20px;
}
</style>
Ahora la búsqueda y el filtrado por categorías pueden funcionar a la vez: después de escribir una palabra clave, al hacer clic en el botón de categoría, los resultados se filtran de forma acumulativa. Ambas condiciones son reactivas; si cualquiera de las dos cambia, filteredArticles se actualiza automáticamente.
Resumen del capítulo
En este capítulo has aprendido a usar watch (especificar manualmente la fuente de datos y obtener los valores anterior y nuevo) y watchEffect (seguimiento automático de dependencias, ejecución inmediata), y has aprendido a implementar el filtrado combinado de búsqueda + categoría con computed.
La experiencia de filtrado en tiempo real en el cuadro de búsqueda es mucho más fluida que «escribir y luego hacer clic en un botón»: este es el valor del sistema reactivo.
Otros temas