watch и функциональность поиска

В этой главе вы научитесь использовать watch и watchEffect для отслеживания изменений данных и реализации фильтрации в реальном времени в поле поиска блога.


В каких сценариях нужен watch?

В большинстве случаев реактивного связывания Vue достаточно: при изменении данных шаблон обновляется автоматически.

Но в некоторых сценариях вам нужнопроактивно выполнить часть логики при изменении данных, например:

  • После ввода в поле поиска фильтровать список статей
  • Когда пользователь переключает категории, обновлять заголовок страницы
  • После изменения данных сохранять в localStorage
  • При изменении полей формы выполнять валидацию формы

Именно тогда вам нужныwatchиwatchEffect。


watch — точное отслеживание

watchОтслеживает указанные источники данных и выполняет функцию обратного вызова при изменении данных.

Вы можете получитьновое значениеистарое значениедля сравнения и обработки.

Пример

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

const keyword = ref('')

// watch(отслеживаемый источник данных, функция обратного вызова)
watch(keyword, (newVal, oldVal) => {
console.log(`Ключевое слово поиска изменилось с "${oldVal}" на "${newVal}"`)
// Выполните здесь логику фильтрации
})
</script>

<template>
  <input v-model="keyword" placeholder="Поиск статей...">
  <p>Текущий поиск: {{ keyword }}</p>
</template>

Три источника данных для watch

Тип источника данныхСинтаксисПример
Одиночный refwatch(ref, callback)watch(keyword, fn)
Свойство реактивного объектаwatch(() => obj.prop, callback)watch(() => user.name, fn)
Несколько источников данныхwatch([ref1, ref2], callback)watch([kw, cat], fn)

Пример

import { ref, watch } from 'vue'

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

// Отслеживать один ref
watch(keyword, (newVal) => {
  document.title = newVal ?`Search: ${newVal}` : 'EXAMPLE Blog'
})

// Отслеживать несколько источников данных
watch([ключевое слово, категория], ([newKw, newCat], [oldKw, oldCat]) => {
  console.log('Ключевое слово или категория изменены, повторная фильтрация статей')
  // Выполнить логику фильтрации...
})

По умолчанию watch работаетлениво—он не срабатывает, когда данные впервые получают значение, а только при последующих изменениях. Если нужно, чтобы он выполнился один раз при инициализации, добавьте{ immediate: true }параметр конфигурации.


watchEffect — автоматически отслеживает зависимости

watchEffectНе нужно вручную указывать источники данных; он автоматически отслеживает реактивные данные, используемые в колбэке.

Самое большое отличие от watch:watchEffect выполняется сразу же один раз。

Пример

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

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

const filteredArticles = ref([])

// watchEffect автоматически отслеживает keyword и articles
// И он сразу выполняется один раз для инициализации filteredArticles
watchEffect(() => {
// Все реактивные данные, используемые в функции, автоматически отслеживаются
  filteredArticles.value = articles.value.filter(
    a => a.title.includes(keyword.value)
  )
console.log(`Результат фильтрации: ${filteredArticles.value.length} статей`)
})
</script>

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

Сравнение watch и watchEffect

ХарактеристикаwatchwatchEffect
Указан ли источник данныхУказывается вручнуюАвтоматически отслеживает зависимости
Первоначальное выполнениеПо умолчанию не выполняется (настраивается с помощью immediate)Выполняется сразу
Получение старого значенияМожно получить старое значениеНельзя получить старое значение
Применимые сценарииКогда нужно сравнить старое и новое значения, точно контролировать момент выполненияКогда вам не важны старые/новые значения, а нужно просто «реагировать на изменения»

Простое руководство по выбору: если ваша логика — «когда X меняется, сделай Y», используйте watch. Если ваша логика — «как только любая зависимость меняется, пересчитай результат», используйте watchEffect или computed. На практике для данных, которые можно вычислить, сначала следует использовать computed; watch предназначен для обработки «побочных эффектов» — например, вызова API, локального сохранения, манипулирования DOM.


Подход с debounce

Каждое нажатие клавиши в поле поиска запускает фильтрацию. Это нормально для локальных данных, но если каждый ввод отправляет сетевой запрос, это тратит много ресурсов.

DebounceИдея: подождать, пока пользователь не перестанет печатать в течение некоторого периода, затем выполнить операцию.

Пример

import { ref, watch } from 'vue'

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

let timer = null   // ID таймера

watch(keyword, (newVal) => {
  // Очистить предыдущий таймер
  clearTimeout(timer)
  // Выполнять фильтрацию, только если в течение 300ms не появилось нового изменения ввода
  timer = setTimeout(() => {
    filteredArticles.value = articles.value.filter(
      a => a.title.includes(newVal) || a.summary.includes(newVal)
    )
  }, 300)
})

Для этого проекта блога данные локальные, поэтому debounce не нужен. Но понимание этого подхода очень пригодится в дальнейшем для более крупных проектов.


Практика: добавьте поиск в реальном времени в блог

Добавьте поле поиска в компонент NavBar, чтобы фильтровать заголовки и краткие описания статей в реальном времени по ключевому слову.

Пример

<!-- Путь к файлу: 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([]) // Все статьи
const isLoading = ref(true)
const activeCategory = ref('All')
const keyword = ref('') // Поисковый запрос

// Извлечь все категории
const categories = computed(() => {
  const cats = articles.value.map(a => a.category)
return ['Все', ...new Set(cats)]
})

// computed автоматически отслеживает изменения keyword и activeCategory
// Если любой из них изменится, результат фильтра будет автоматически пересчитан
const filteredArticles = computed(() => {
  let result = articles.value

// Шаг 1: Фильтрация по категории
if (activeCategory.value !== 'All') {
    result = result.filter(a => a.category === activeCategory.value)
  }

// Шаг 2: Фильтрация по ключевому слову (поиск по заголовку и краткому описанию)
  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
})

// Переключение категорий не сбрасывает поиск; эти две функции работают совместно
function handleCategoryChange(cat) {
  activeCategory.value = cat
}

// Загрузить данные
onMounted(async () => {
  try {
    const res = await fetch('/posts.json')
    articles.value = await res.json()
  } catch (err) {
console.error('Ошибка загрузки', err)
  } finally {
    isLoading.value = false
  }
})
</script>

<template>
  <div class="home">
    <!-- Поле поиска -->
    <div class="search-bar">
      <input
       v-model="keyword"
       type="text"
       placeholder="Поиск по заголовку или краткому описанию статьи..."
       class="search-input"
     />
      <span v-if="keyword" class="clear-btn" @click="keyword = ''">✕</span>
    </div>

    <!-- Фильтр по категориям -->
    <CategoryFilter
     :categories="categories"
     :active-category="activeCategory"
     @update-category="handleCategoryChange"
   />

    <!-- Загрузка / Пусто / Обычное -->
    <p v-if="isLoading">Загрузка...</p>
    <p v-else-if="filteredArticles.length === 0">Подходящие статьи не найдены</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>

Теперь поиск и фильтрация по категориям работают вместе—после ввода поискового ключевого слова нажатие кнопки категории будет накладывать фильтры. Оба условия реактивны; когда любое из них изменяется, filteredArticles обновляется автоматически.


Краткое содержание главы

В этой главе вы освоили watch (вручную указывать источник данных, можно получать старые и новые значения) и watchEffect (автоматически отслеживает зависимости, выполняется сразу), а также научились использовать computed для комбинированного поиска + фильтрации по категориям.

Опыт фильтрации в реальном времени с помощью поисковой строки гораздо плавнее, чем «введи и нажми кнопку»—в этом и заключается ценность реактивной системы.

Другие расширения