6. computed и watch
computed и watch — вычисления и наблюдатели 🔄
Заголовок раздела «computed и watch — вычисления и наблюдатели 🔄»В Vue 3 есть два мощных инструмента для работы с реактивными данными: computed — для вычисленных значений (кешируется, не делает побочных эффектов), и watch — для реакции на изменения (делает побочные эффекты: запросы, сохранение, логирование). Важно не путать их назначение! 🎯
computed() — кешированные вычисления 🧮
Заголовок раздела «computed() — кешированные вычисления 🧮»Представь computed как «умную формулу»: пересчитывается только когда зависимости изменились:
import { ref, computed } from 'vue'
const firstName = ref('Яша')const lastName = ref('Смирнов')const items = ref([1, 2, 3, 4, 5])
// Простой computed — только getterconst fullName = computed(() => { console.log('Пересчёт fullName...') return `${firstName.value} ${lastName.value}`})
// Computed от массиваconst sum = computed(() => items.value.reduce((a, b) => a + b, 0))const evenItems = computed(() => items.value.filter(n => n % 2 === 0))
// Computed возвращает ComputedRef<T>console.log(fullName.value) // 'Яша Смирнов'console.log(sum.value) // 15Кеширование — главное преимущество
Заголовок раздела «Кеширование — главное преимущество»const count = ref(0)
// ✅ computed — кешируется!const doubled = computed(() => { console.log('expensive calculation!') return count.value * 2})
// В шаблоне можно использовать 100 раз — вычислится ОДИН раз// {{ doubled }} {{ doubled }} {{ doubled }} // только 1 вызов!
// ❌ Метод — вызывается КАЖДЫЙ раз при рендереconst getDoubled = () => { console.log('expensive calculation!') return count.value * 2}// {{ getDoubled() }} {{ getDoubled() }} // 2 вызова при каждом рендере!computed с getter и setter 🔧
Заголовок раздела «computed с getter и setter 🔧»import { ref, computed } from 'vue'
const firstName = ref('Яша')const lastName = ref('Смирнов')
// Computed с getter + setterconst fullName = computed({ // getter — читаем computed get() { return `${firstName.value} ${lastName.value}` }, // setter — записываем в computed set(newValue: string) { const parts = newValue.split(' ') firstName.value = parts[0] || '' lastName.value = parts.slice(1).join(' ') || '' }})
// Чтениеconsole.log(fullName.value) // 'Яша Смирнов'
// Запись — вызывает setter!fullName.value = 'Иван Петров'console.log(firstName.value) // 'Иван'console.log(lastName.value) // 'Петров'Применение в v-model:
<template> <!-- v-model на computed с setter! --> <input v-model="fullName" /> <p>Имя: {{ firstName }}, Фамилия: {{ lastName }}</p></template>Lazy computed с производительностью 🚀
Заголовок раздела «Lazy computed с производительностью 🚀»import { ref, computed } from 'vue'
const hugeList = ref<number[]>(Array.from({ length: 10000 }, (_, i) => i))const sortOrder = ref<'asc' | 'desc'>('asc')
// Computed пересчитывается только когда hugeList или sortOrder изменятсяconst sortedList = computed(() => { return [...hugeList.value].sort((a, b) => sortOrder.value === 'asc' ? a - b : b - a )})
// Первое обращение — вычисляемconsole.log(sortedList.value) // [0, 1, 2, ..., 9999]
// Второе обращение без изменений — КЕSH, не вычисляем!console.log(sortedList.value) // из кеша мгновенно ✅
// Изменяем зависимость — пересчитываемsortOrder.value = 'desc'console.log(sortedList.value) // [9999, ..., 1, 0] — пересчётwatch() — наблюдатели ⌚
Заголовок раздела «watch() — наблюдатели ⌚»watch реагирует на изменения и выполняет побочные эффекты:
import { ref, watch } from 'vue'
const count = ref(0)const searchQuery = ref('')
// Простой watch — одна переменнаяwatch(count, (newValue, oldValue) => { console.log(`count: ${oldValue} → ${newValue}`) // Здесь можно: fetch, localStorage, логирование})
// Watch строки с дебаунсомlet timeout: ReturnType<typeof setTimeout>watch(searchQuery, (query) => { clearTimeout(timeout) timeout = setTimeout(() => { fetchResults(query) }, 300)})Опции watch: immediate и deep
Заголовок раздела «Опции watch: immediate и deep»import { ref, reactive, watch } from 'vue'
const userId = ref(1)const user = reactive({ name: 'Яша', settings: { theme: 'dark' } })
// immediate: true — вызвать СРАЗУ при монтированииwatch(userId, async (id) => { user.name = await fetchUserName(id)}, { immediate: true })
// deep: true — отслеживать вложенные изменения в объектеwatch(user, (newUser) => { localStorage.setItem('user', JSON.stringify(newUser))}, { deep: true })
// Без deep — user.settings.theme не вызовет watcher!watch(() => user.settings.theme, (theme) => { document.body.className = theme})Watch нескольких источников
Заголовок раздела «Watch нескольких источников»import { ref, watch } from 'vue'
const firstName = ref('Яша')const lastName = ref('Смирнов')const age = ref(25)
// Массив источников — вызывается если ЛЮБОЙ из них изменилсяwatch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => { console.log(`Имя: ${oldFirst} ${oldLast} → ${newFirst} ${newLast}`)})
// С getter функциямиwatch( [() => firstName.value, () => age.value], ([name, age]) => { updateProfile(name, age) })Остановка watcher
Заголовок раздела «Остановка watcher»import { ref, watch, onUnmounted } from 'vue'
const count = ref(0)
// watch возвращает функцию остановкиconst stopWatcher = watch(count, (val) => { console.log('count changed:', val)})
// Останавливаем через 5 секундsetTimeout(stopWatcher, 5000)
// Или при размонтировании компонента (делается автоматически!)// Но если создаёшь watcher асинхронно — нужно останавливать вручнуюlet watcher: (() => void) | null = null
const startWatching = async () => { await someAsyncOperation() // Этот watcher НЕ будет автоматически остановлен! watcher = watch(count, (val) => console.log(val))}
onUnmounted(() => { watcher?.() // останавливаем вручную})watchEffect() — авто-отслеживание зависимостей ✨
Заголовок раздела «watchEffect() — авто-отслеживание зависимостей ✨»watchEffect запускается сразу и автоматически отслеживает все refs внутри:
import { ref, watchEffect } from 'vue'
const userId = ref(1)const user = ref(null)
// Запускается СРАЗУ (не нужен immediate: true!)// Автоматически отслеживает userId и перезапускается при измененииwatchEffect(async () => { // userId.value используется внутри — Vue видит зависимость! user.value = await fetchUser(userId.value)})Когда use watchEffect вместо watch?
// watch — когда знаешь источник и хочешь старое значениеwatch(count, (newVal, oldVal) => { console.log(`${oldVal} → ${newVal}`)})
// watchEffect — когда несколько источников и не нужно oldValwatchEffect(() => { document.title = `${firstName.value} ${lastName.value} | Моё приложение` // Автоматически перезапустится при изменении firstName ИЛИ lastName})watchPostEffect() и onWatcherCleanup() 🧹
Заголовок раздела «watchPostEffect() и onWatcherCleanup() 🧹»import { ref, watchEffect, watchPostEffect, onWatcherCleanup } from 'vue'
const searchQuery = ref('')
// watchPostEffect — запускается ПОСЛЕ обновления DOMwatchPostEffect(() => { // Здесь DOM уже обновлён! const element = document.querySelector('.search-results') element?.scrollTo({ top: 0 })})
// Cleanup — очистка при следующем запуске или размонтированииwatchEffect(() => { const controller = new AbortController()
fetch(`/api/search?q=${searchQuery.value}`, { signal: controller.signal }) .then(r => r.json()) .then(data => { /* обновляем результаты */ })
// onWatcherCleanup — отменяем предыдущий запрос! onWatcherCleanup(() => { controller.abort() // отменяем fetch при новом запросе })})Практические паттерны 💡
Заголовок раздела «Практические паттерны 💡»Паттерн 1: Fetch при изменении route параметра
Заголовок раздела «Паттерн 1: Fetch при изменении route параметра»import { ref, watch } from 'vue'import { useRoute } from 'vue-router'
const route = useRoute()const post = ref(null)const loading = ref(false)
watch( () => route.params.id, async (id) => { if (!id) return loading.value = true post.value = await fetchPost(id) loading.value = false }, { immediate: true } // загрузить сразу при монтировании)Паттерн 2: Синхронизация с localStorage
Заголовок раздела «Паттерн 2: Синхронизация с localStorage»import { ref, watch } from 'vue'
const theme = ref<'light' | 'dark'>( (localStorage.getItem('theme') as 'light' | 'dark') || 'light')
// Автоматически сохраняем в localStorage при измененииwatch(theme, (newTheme) => { localStorage.setItem('theme', newTheme) document.documentElement.setAttribute('data-theme', newTheme)}, { immediate: true }) // применяем сразу при загрузкеПаттерн 3: Дебаунс поиска
Заголовок раздела «Паттерн 3: Дебаунс поиска»import { ref, watch } from 'vue'
const searchQuery = ref('')const results = ref([])const isSearching = ref(false)
watch(searchQuery, (query) => { if (!query.trim()) { results.value = [] return } isSearching.value = true})
// Дебаунс через watchEffectlet searchTimeout: ReturnType<typeof setTimeout>watchEffect(() => { const query = searchQuery.value
clearTimeout(searchTimeout) searchTimeout = setTimeout(async () => { if (!query.trim()) return const data = await searchAPI(query) results.value = data isSearching.value = false }, 300)})Паттерн 4: Computed с валидацией формы
Заголовок раздела «Паттерн 4: Computed с валидацией формы»import { reactive, computed } from 'vue'
const form = reactive({ email: '', password: '', confirmPassword: ''})
const errors = computed(() => { const errs: Record<string, string> = {}
if (!form.email.includes('@')) { errs.email = 'Некорректный email' }
if (form.password.length < 8) { errs.password = 'Минимум 8 символов' }
if (form.password !== form.confirmPassword) { errs.confirmPassword = 'Пароли не совпадают' }
return errs})
const isValid = computed(() => Object.keys(errors.value).length === 0)
const hasError = (field: string) => computed(() => !!errors.value[field])computed vs watch — итоговая таблица 📊
Заголовок раздела «computed vs watch — итоговая таблица 📊»| Критерий | computed | watch |
|---|---|---|
| Цель | Вычислить значение | Реагировать на изменения |
| Кеш | ✅ Да | ❌ Нет |
| Побочные эффекты | ❌ Нельзя (fetch, DOM) | ✅ Основное применение |
| Старое значение | ❌ Нет | ✅ Второй аргумент |
| immediate | Всегда вычисляется | По умолчанию нет |
| Async | ⚠️ Не рекомендуется | ✅ Отлично |
| Несколько источников | Автоматически | Массив источников |
Резюме 🎯
Заголовок раздела «Резюме 🎯»computed()— для вычислений без побочных эффектов, кешируетсяcomputed({ get, set })— вычисление с setter для v-modelwatch(source, handler, options)— реакция на конкретные измененияwatchEffect(fn)— автоматическое отслеживание, запуск сразуwatchPostEffect(fn)— после обновления DOMonWatcherCleanup(fn)— очистка ресурсов (отмена fetch, etc.)
Следующий урок — шаблоны и директивы! 📄