watch et fonctionnalité de recherche

Dans ce chapitre, vous apprendrez à utiliser watch et watchEffect pour écouter les changements de données et implémenter un filtrage en temps réel dans la boîte de recherche du blog.


Quels scénarios nécessitent watch ?

Dans la plupart des cas, la liaison réactive de Vue est suffisante : lorsque les données changent, le template se met à jour automatiquement.

Mais certains scénarios nécessitent que vousexécutiez proactivement une logique lorsque les données changent, tels que :

  • Après avoir saisi dans la boîte de recherche, filtrer la liste des articles
  • Lorsque l'utilisateur change de catégorie, mettre à jour le titre de la page
  • Après modification des données, enregistrer dans localStorage
  • Lorsque les champs du formulaire changent, effectuer la validation du formulaire

C'est alors que vous avez besoin dewatchetwatchEffect。


watch — Écoute précise

watchÉcoutez les sources de données spécifiées et exécutez une fonction de rappel lorsque les données changent.

Vous pouvez obtenir lanouvelle valeuretl'ancienne valeur, pour un traitement comparatif.

Exemple

<script setup>
import { ref, watch } from 'vue'

const keyword = ref('')

// watch(la source de données écoutée, fonction de rappel)
watch(keyword, (newVal, oldVal) => {
console.log(`Le mot-clé de recherche est passé de "${oldVal}" à "${newVal}"`)
// Exécuter la logique de filtrage ici
})
</script>

<template>
  <input v-model="keyword" placeholder="Rechercher des articles...">
  <p>Recherche actuelle : {{ keyword }}</p>
</template>

Trois sources de données pour watch

Type de source de donnéesSyntaxeExemple
Ref uniquewatch(ref, callback)watch(keyword, fn)
Propriété d'un objet réactifwatch(() => obj.prop, callback)watch(() => user.name, fn)
Sources de données multipleswatch([ref1, ref2], callback)watch([kw, cat], fn)

Exemple

import { ref, watch } from 'vue'

const keyword = ref('')
const category = ref('All')

// Écouter une seule ref
watch(keyword, (newVal) => {
  document.title = newVal ?`Search: ${newVal}` : 'EXAMPLE Blog'
})

// Écouter plusieurs sources de données
watch([mot-clé, catégorie], ([newKw, newCat], [oldKw, oldCat]) => {
  console.log('Mot-clé ou catégorie modifiés, re-filtrage des articles')
  // Exécuter la logique de filtrage...
})

Par défaut, watch estexécuté paresseusement—il ne se déclenchera pas lorsque les données ont une valeur pour la première fois, uniquement lors des changements suivants. Si vous avez besoin qu'il s'exécute une fois lors de l'initialisation, ajoutez l'{ immediate: true }option de configuration.


watchEffect — Suit automatiquement les dépendances

watchEffectPas besoin de spécifier manuellement les sources de données ; il suit automatiquement les données réactives utilisées dans le callback.

La plus grande différence avec watch :watchEffect s'exécute immédiatement une première fois。

Exemple

<script setup>
import { ref, watchEffect } from 'vue'

const keyword = ref('')
const articles = ref([
{ id: 1, title: 'Introduction à Vue3' },
{ id: 2, title: 'JS asynchrone' },
])

const filteredArticles = ref([])

// watchEffect suit automatiquement keyword et articles
// Et il s'exécute une fois immédiatement pour initialiser filteredArticles
watchEffect(() => {
// Toutes les données réactives utilisées dans la fonction sont automatiquement suivies
  filteredArticles.value = articles.value.filter(
    a => a.title.includes(keyword.value)
  )
console.log(`Résultat du filtre : ${filteredArticles.value.length} articles`)
})
</script>

<template>
  <input v-model="keyword" placeholder="Rechercher">
  <div v-for="a in filteredArticles" :key="a.id">
    {{ a.title }}
  </div>
</template>

Comparaison entre watch et watchEffect

FonctionnalitéwatchwatchEffect
Si la source de données est spécifiéeSpécifiée manuellementSuit automatiquement les dépendances
Exécution initialeNe s'exécute pas par défaut (configurable avec immediate)S'exécute immédiatement
Obtention de l'ancienne valeurPeut obtenir l'ancienne valeurNe peut pas obtenir l'ancienne valeur
Scénarios applicablesLorsque vous devez comparer les anciennes et nouvelles valeurs et contrôler précisément le momentLorsque vous ne vous souciez pas des anciennes/nouvelles valeurs, vous voulez simplement « réagir aux changements »

Guide de sélection simple : si votre logique est « quand X change, faites Y », utilisez watch. Si votre logique est « dès que l'une des dépendances change, recalculez le résultat », utilisez watchEffect ou computed. En pratique, les données qui peuvent être calculées doivent utiliser computed en priorité ; watch sert à gérer les « effets de bord » — comme appeler des API, sauvegarder localement, manipuler le DOM.


Approche de debounce

Chaque frappe dans le champ de recherche déclenche le filtrage. C'est acceptable pour des données locales, mais si chaque saisie envoie une requête réseau, cela gaspille beaucoup.

DebounceL'idée : attendre que l'utilisateur arrête de taper pendant un certain temps, puis exécuter l'opération.

Exemple

import { ref, watch } from 'vue'

const keyword = ref('')
const filteredArticles = ref([])

let timer = null   // ID du minuteur

watch(keyword, (newVal) => {
  // Effacer le minuteur précédent
  clearTimeout(timer)
  // Exécuter le filtrage uniquement s'il n'y a pas de nouveau changement de saisie dans les 300ms
  timer = setTimeout(() => {
    filteredArticles.value = articles.value.filter(
      a => a.title.includes(newVal) || a.summary.includes(newVal)
    )
  }, 300)
})

Pour ce projet de blog, les données sont locales, donc le debounce n'est pas nécessaire. Mais comprendre cette approche est très utile pour des projets plus importants plus tard.


Mise en pratique : Ajouter une recherche en temps réel au blog

Ajouter une zone de recherche au composant NavBar pour filtrer en temps réel les titres et résumés d'articles par mot-clé.

Exemple

<!-- Chemin de fichier : src/views/HomeView.vue -->
<script setup>
import { ref, computed, onMounted } from 'vue'
import BlogCard from '../components/BlogCard.vue'
import CategoryFilter from '../components/CategoryFilter.vue'

const articles = ref([]) // Tous les articles
const isLoading = ref(true)
const activeCategory = ref('Toutes')
const keyword = ref('') // Mot-clé de recherche

// Extraire toutes les catégories
const categories = computed(() => {
  const cats = articles.value.map(a => a.category)
return ['Toutes', ...new Set(cats)]
})

// computed suit automatiquement les modifications de keyword et activeCategory
// Si l'un ou l'autre change, le résultat du filtre sera automatiquement recalculé
const filteredArticles = computed(() => {
  let result = articles.value

// Étape 1 : Filtrer par catégorie
if (activeCategory.value !== 'All') {
    result = result.filter(a => a.category === activeCategory.value)
  }

// Étape 2 : Filtrer par mot-clé (rechercher dans le titre et le résumé)
  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
})

// Changer de catégorie n'efface pas la recherche ; les deux fonctionnent en combinaison
function handleCategoryChange(cat) {
  activeCategory.value = cat
}

// Charger les données
onMounted(async () => {
  try {
    const res = await fetch('/posts.json')
    articles.value = await res.json()
  } catch (err) {
console.error('Load failed', err)
  } finally {
    isLoading.value = false
  }
})
</script>

<template>
  <div class="home">
    <!-- Boîte de recherche -->
    <div class="search-bar">
      <input
       v-model="keyword"
       type="text"
       placeholder="Rechercher un titre ou un résumé d'article..."
       class="search-input"
     />
      <span v-if="keyword" class="clear-btn" @click="keyword = ''">✕</span>
    </div>

    <!-- Filtre par catégorie -->
    <CategoryFilter
     :categories="categories"
     :active-category="activeCategory"
     @update-category="handleCategoryChange"
   />

    <!-- Chargement / Vide / Normal -->
    <p v-if="isLoading">Chargement...</p>
    <p v-else-if="filteredArticles.length === 0">Aucun article correspondant trouvé</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>

Maintenant, la recherche et le filtrage par catégorie fonctionnent ensemble — après avoir saisi un mot-clé de recherche, cliquer sur un bouton de catégorie empilera les filtres. Les deux conditions sont réactives ; lorsque l'une ou l'autre change, filteredArticles se met à jour automatiquement.


Résumé du chapitre

Dans ce chapitre, vous avez maîtrisé watch (spécifier manuellement la source de données, peut obtenir les anciennes et nouvelles valeurs) et watchEffect (suit automatiquement les dépendances, s'exécute immédiatement), et vous avez appris à utiliser computed pour combiner recherche + filtrage par catégorie.

L'expérience de filtrage en temps réel d'une zone de recherche est bien plus fluide que « taper et cliquer sur un bouton » — c'est la valeur d'un système réactif.

Autres extensions