Перейти к содержимому

6. computed и watch

В Vue 3 есть два мощных инструмента для работы с реактивными данными: computed — для вычисленных значений (кешируется, не делает побочных эффектов), и watch — для реакции на изменения (делает побочные эффекты: запросы, сохранение, логирование). Важно не путать их назначение! 🎯


Представь computed как «умную формулу»: пересчитывается только когда зависимости изменились:

import { ref, computed } from 'vue'
const firstName = ref('Яша')
const lastName = ref('Смирнов')
const items = ref([1, 2, 3, 4, 5])
// Простой computed — только getter
const 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 вызова при каждом рендере!

import { ref, computed } from 'vue'
const firstName = ref('Яша')
const lastName = ref('Смирнов')
// Computed с getter + setter
const 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>

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 реагирует на изменения и выполняет побочные эффекты:

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)
})
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
})
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)
}
)
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 — когда несколько источников и не нужно oldVal
watchEffect(() => {
document.title = `${firstName.value} ${lastName.value} | Моё приложение`
// Автоматически перезапустится при изменении firstName ИЛИ lastName
})

import { ref, watchEffect, watchPostEffect, onWatcherCleanup } from 'vue'
const searchQuery = ref('')
// watchPostEffect — запускается ПОСЛЕ обновления DOM
watchPostEffect(() => {
// Здесь 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 при новом запросе
})
})

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 } // загрузить сразу при монтировании
)
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 }) // применяем сразу при загрузке
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
})
// Дебаунс через watchEffect
let 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)
})
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])

Критерийcomputedwatch
ЦельВычислить значениеРеагировать на изменения
Кеш✅ Да❌ Нет
Побочные эффекты❌ Нельзя (fetch, DOM)✅ Основное применение
Старое значение❌ Нет✅ Второй аргумент
immediateВсегда вычисляетсяПо умолчанию нет
Async⚠️ Не рекомендуется✅ Отлично
Несколько источниковАвтоматическиМассив источников

  • computed() — для вычислений без побочных эффектов, кешируется
  • computed({ get, set }) — вычисление с setter для v-model
  • watch(source, handler, options) — реакция на конкретные изменения
  • watchEffect(fn) — автоматическое отслеживание, запуск сразу
  • watchPostEffect(fn) — после обновления DOM
  • onWatcherCleanup(fn) — очистка ресурсов (отмена fetch, etc.)

Следующий урок — шаблоны и директивы! 📄