watch и функциональность поиска
В этой главе вы научитесь использовать watch и watchEffect для отслеживания изменений данных и реализации фильтрации в реальном времени в поле поиска блога.
В каких сценариях нужен watch?
В большинстве случаев реактивного связывания Vue достаточно: при изменении данных шаблон обновляется автоматически.
Но в некоторых сценариях вам нужнопроактивно выполнить часть логики при изменении данных, например:
- После ввода в поле поиска фильтровать список статей
- Когда пользователь переключает категории, обновлять заголовок страницы
- После изменения данных сохранять в localStorage
- При изменении полей формы выполнять валидацию формы
Именно тогда вам нужныwatchиwatchEffect。
watch — точное отслеживание
watchОтслеживает указанные источники данных и выполняет функцию обратного вызова при изменении данных.
Вы можете получитьновое значениеистарое значениедля сравнения и обработки.
Пример
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
| Тип источника данных | Синтаксис | Пример |
|---|---|---|
| Одиночный ref | watch(ref, callback) | watch(keyword, fn) |
| Свойство реактивного объекта | watch(() => obj.prop, callback) | watch(() => user.name, fn) |
| Несколько источников данных | watch([ref1, ref2], callback) | watch([kw, cat], fn) |
Пример
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 выполняется сразу же один раз。
Пример
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
| Характеристика | watch | watchEffect |
|---|---|---|
| Указан ли источник данных | Указывается вручную | Автоматически отслеживает зависимости |
| Первоначальное выполнение | По умолчанию не выполняется (настраивается с помощью immediate) | Выполняется сразу |
| Получение старого значения | Можно получить старое значение | Нельзя получить старое значение |
| Применимые сценарии | Когда нужно сравнить старое и новое значения, точно контролировать момент выполнения | Когда вам не важны старые/новые значения, а нужно просто «реагировать на изменения» |
Простое руководство по выбору: если ваша логика — «когда X меняется, сделай Y», используйте watch. Если ваша логика — «как только любая зависимость меняется, пересчитай результат», используйте watchEffect или computed. На практике для данных, которые можно вычислить, сначала следует использовать computed; watch предназначен для обработки «побочных эффектов» — например, вызова API, локального сохранения, манипулирования DOM.
Подход с debounce
Каждое нажатие клавиши в поле поиска запускает фильтрацию. Это нормально для локальных данных, но если каждый ввод отправляет сетевой запрос, это тратит много ресурсов.
DebounceИдея: подождать, пока пользователь не перестанет печатать в течение некоторого периода, затем выполнить операцию.
Пример
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, чтобы фильтровать заголовки и краткие описания статей в реальном времени по ключевому слову.
Пример
<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 для комбинированного поиска + фильтрации по категориям.
Опыт фильтрации в реальном времени с помощью поисковой строки гораздо плавнее, чем «введи и нажми кнопку»—в этом и заключается ценность реактивной системы.
Другие расширения