Олег Марков
Watch и WatchEffect в Vue 3 - подробный разбор с примерами
Введение
Watch и WatchEffect в Vue 3 — это два близких по идее, но разных по поведению инструмента для работы с реактивностью. Оба они позволяют запускать побочные эффекты при изменении данных, но делают это по-разному и подходят для разных сценариев.
В этой статье вы увидите, чем именно отличаются watch и watchEffect, в каких ситуациях лучше использовать каждый из них, какие настройки доступны и какие типичные ошибки чаще всего возникают у разработчиков. Я буду опираться на Composition API (setup, ref, reactive), но по ходу статьи отмечу и особенности Options API.
Что такое реактивное наблюдение в Vue
Прежде всего давайте коротко уточним, что именно здесь подразумевается под «наблюдением».
Реактивность в двух словах
В Vue любые данные, созданные через ref или reactive, становятся реактивными. Это значит, что Vue отслеживает чтение и запись этих значений и может автоматически обновлять шаблон или выполнять другие действия.
import { ref, reactive } from 'vue'
const count = ref(0) // реактивное примитивное значение
const user = reactive({ // реактивный объект
name: 'Alex',
age: 30
})
- Когда вы читаете
count.value, Vue запоминает это место как зависимость. - Когда вы меняете
count.value = 1, Vue понимает, что зависимость изменилась, и обновляет все, что подписано на это значение (компонент, computed, watcher и т.д.).
watch и watchEffect — это как раз механизмы «подписки» на такие изменения, но с разной степенью явности и контроля.
Watch — явное наблюдение за конкретными источниками
watch позволяет вам явно указать, за чем именно нужно наблюдать. Это может быть:
- одно реактивное значение (ref),
- функция, возвращающая нужное значение,
- массив источников.
Базовый синтаксис watch
Сигнатура в упрощенном виде:
watch(source, callback, options?)
source— за чем следим (источник).callback(newValue, oldValue, onCleanup)— что делаем при изменении.options— дополнительные настройки (например,immediate,deepи др.).
Давайте разберемся на простом примере.
import { ref, watch } from 'vue'
export default {
setup() {
const count = ref(0)
// Здесь мы настраиваем наблюдатель за count
watch(
count, // источник - за ним следим
(newValue, oldValue) => { // колбэк - вызывается при изменении
console.log('count изменился', oldValue, '→', newValue)
}
)
// Функция, чтобы менять count
const increment = () => {
count.value++ // после изменения сработает watch
}
return { count, increment }
}
}
Как видите, этот код выполняет простой сценарий: мы явно указали источник (count), и Vue вызовет колбэк при каждом изменении count.value.
Watch по функции-источнику
Часто удобнее следить не за целым объектом, а за его частью. Для этого в watch можно передать функцию, которая возвращает нужное значение.
import { reactive, watch } from 'vue'
export default {
setup() {
const user = reactive({
name: 'Alex',
age: 30
})
// Здесь мы следим только за возрастом, а не за всем объектом user
watch(
() => user.age, // функция-источник
(newAge, oldAge) => {
console.log('Возраст изменился', oldAge, '→', newAge)
}
)
const growUp = () => {
user.age++ // триггерит watcher
}
return { user, growUp }
}
}
Почему это полезно:
- меньше бессмысленных срабатываний, если вы меняете другие поля
user, - проще контролировать, что именно вы наблюдаете,
- лучше читаемость: сразу видно, за чем следите.
Наблюдение за несколькими источниками
watch умеет следить сразу за несколькими значениями, если передать массив источников.
import { ref, watch } from 'vue'
export default {
setup() {
const firstName = ref('Alex')
const lastName = ref('Smith')
// Здесь мы следим сразу за двумя значениями
watch(
[firstName, lastName], // массив источников
([newFirst, newLast], [oldFirst, oldLast]) => {
console.log('Имя или фамилия изменились')
console.log('Было', oldFirst, oldLast)
console.log('Стало', newFirst, newLast)
}
)
return { firstName, lastName }
}
}
Вы можете комбинировать ref, reactive и функции:
watch(
[() => user.age, firstName], // смесь функции и ref
([newAge, newFirst]) => {
// Реакция, когда изменился возраст или имя
}
)
Основные опции watch
Теперь давайте посмотрим, какие настройки позволяют вам контролировать поведение watch.
immediate — запускать ли колбэк сразу
По умолчанию watch вызывает колбэк только при первом изменении значения. Если вам нужно, чтобы колбэк выполнился сразу после установки наблюдателя (например, для начальной загрузки данных), используйте immediate: true.
watch(
() => user.id,
async (newId) => {
// Здесь мы загружаем данные при каждом изменении id
// и сразу же при первом запуске компонента
await fetchUserProfile(newId)
},
{ immediate: true } // запускаем колбэк сразу
)
Смотрите, что происходит: как только setup отработал и watcher создался, Vue тут же вызывает ваш колбэк с текущим значением источника.
deep — глубокое наблюдение за объектами
Если вы передаете в watch реактивный объект reactive напрямую, Vue отслеживает его поверхностно (shallow). То есть изменения вложенных свойств могут не вызывать перезапуск, если источник — объект без функции-обертки.
Глубокий режим включает рекурсивное отслеживание вложенных свойств.
import { reactive, watch } from 'vue'
export default {
setup() {
const settings = reactive({
theme: {
dark: false
},
language: 'ru'
})
// Здесь я размещаю пример, чтобы вам было проще понять deep-наблюдение
watch(
settings, // наблюдаем весь объект
(newVal, oldVal) => {
console.log('Настройки изменились')
},
{ deep: true } // включаем глубокое наблюдение
)
const enableDarkTheme = () => {
settings.theme.dark = true // это изменение поймает deep watcher
}
return { settings, enableDarkTheme }
}
}
Если бы вы не указали deep: true, изменение settings.theme.dark могло бы не вызвать срабатывания, в зависимости от того, как устроен ваш источник.
Важно: если вы используете watch(() => settings.theme.dark, ...), deep не нужен, потому что функция уже указывает конкретное свойство, и Vue автоматически отслеживает его.
flush — когда именно выполнять колбэк
Опция flush управляет тем, в какой момент цикла обновления Vue выполняется ваш колбэк:
'pre'(поведение по умолчанию) — до обновления DOM,'post'— после обновления DOM,'sync'— синхронно, сразу при изменении зависимости.
watch(
() => count.value,
(newVal) => {
// Здесь мы хотим работать уже с обновленным DOM
console.log('count изменился, DOM уже обновлен')
},
{ flush: 'post' } // выполняем колбэк после обновления DOM
)
flush: 'post' особенно полезен, если вам нужно прочитать размеченный DOM (например, размеры элемента) после обновления данных.
flush: 'sync' стоит использовать очень аккуратно: он может приводить к каскаду синхронных обновлений и потере производительности.
onCleanup — очистка побочных эффектов
Внутри колбэка watch третий аргумент — это функция onCleanup, позволяющая регистрировать очистку для предыдущего эффекта. Это важно, когда вы запускаете асинхронные операции, таймеры или подписки.
watch(
() => searchQuery.value,
(newQuery, _oldQuery, onCleanup) => {
// Здесь мы показываем, как отменять предыдущий запрос
const controller = new AbortController()
// Регистрируем очистку - отмена предыдущего запроса
onCleanup(() => {
controller.abort() // отменяем, если срабатывает новый watcher
})
fetch('/api/search?q=' + newQuery, {
signal: controller.signal
}).then(/* ... */)
}
)
Как видите, этот код решает задачу: «каждый новый запрос отменяет предыдущий», что часто требуется в формах поиска.
WatchEffect — автоматическое отслеживание зависимостей
watchEffect работает по другому принципу: вы не указываете явные источники, Vue сам определяет зависимости во время выполнения эффекта.
Сигнатура:
watchEffect(effect, options?)
effect(onCleanup)— функция, внутри которой вы читаете реактивные данные.- Vue автоматически отслеживает все прочитанные реактивные значения и пересоздает эффект при их изменении.
Простой пример watchEffect
import { ref, watchEffect } from 'vue'
export default {
setup() {
const count = ref(0)
const doubled = ref(0)
// Здесь Vue сам определит зависимость от count.value
watchEffect(() => {
doubled.value = count.value * 2 // чтение count делает его зависимостью
console.log('doubled обновился', doubled.value)
})
const increment = () => {
count.value++ // триггерит watchEffect
}
return { count, doubled, increment }
}
}
Вы не указали явно «следить за count», но при первом запуске watchEffect прочитал count.value, поэтому Vue добавил его в список зависимостей.
Важно: watchEffect всегда запускается сразу при создании (аналог immediate: true для watch).
Где watchEffect особенно удобен
watchEffect хорошо подходит, когда:
- вы делаете простой побочный эффект, зависящий от нескольких реактивных значений,
- вам не нужно знать старое значение,
- зависимости легко считываются автоматически.
Например, логирование или синхронизация с внешним состоянием:
watchEffect(() => {
console.log('Текущее состояние фильтров', {
search: searchQuery.value,
page: currentPage.value
})
})
Здесь я размещаю пример, чтобы вам было проще увидеть, как watchEffect собирает сразу несколько зависимостей.
onCleanup в watchEffect
Как и watch, watchEffect поддерживает очистку через onCleanup, но здесь она передается как аргумент самой функции-эффекта.
watchEffect((onCleanup) => {
const intervalId = setInterval(() => {
console.log('Таймер тикает')
}, 1000)
// Регистрируем очистку, чтобы остановить таймер
onCleanup(() => {
clearInterval(intervalId)
})
})
Каждый раз, когда зависимости эффекта меняются, Vue сначала вызывает очистку предыдущего эффекта, а затем запускает его снова.
Сравнение watch и watchEffect
Теперь давайте посмотрим на отличие этих двух инструментов с практической точки зрения.
Явные vs неявные зависимости
watch— вы явно указываете, за чем следите.watchEffect— Vue автоматически отслеживает все прочитанные реактивные значения.
Это дает разные плюсы и минусы.
Когда лучше использовать watch
- Вам нужно знать старое и новое значение.
- Вам нужно следить только за определенными полями, а не за всем, что случайно будет прочитано.
- Вы хотите минимизировать лишние срабатывания.
- Вам важно точное управление, например
deep,immediate, массив источников.
Пример загрузки данных по ID:
watch(
() => userId.value,
(newId, oldId) => {
if (newId !== oldId) {
loadUser(newId)
}
},
{ immediate: true }
)
Когда лучше использовать watchEffect
- Вы хотите описать «реакцию» в одном месте и не думать о перечислении зависимостей.
- Вы не используете
oldValue, вам оно не нужно. - Вы согласны на то, что эффект будет срабатывать чаще и должен быть «легким».
Пример синхронизации локального состояния с чем-то внешним:
watchEffect(() => {
localStorage.setItem('filters', JSON.stringify({
search: searchQuery.value,
sort: sortOrder.value
}))
})
Как видите, этот код выполняет задачу синхронизации без перечисления зависимостей. Все, что вы прочитали внутри watchEffect, будет отслеживаться автоматически.
Контроль vs простота
watchдает больше контроля, но требует от вас чуть больше кода (источники, опции).watchEffectкороче и проще, но может быть менее предсказуем по набору зависимостей, особенно если внутри есть условные ветки.
Поэтому часто рекомендуют такой подход:
- Начинать с
watchEffect, когда эффект простой. - Переходить на
watch, когда:- нужны
oldValue, - нужно явно ограничить набор зависимостей,
- есть сложная логика с условиями и несколькими полями.
- нужны
Глубокое наблюдение и работа с объектами
Работа с объектами — одна из частых зон путаницы. Давайте разберем несколько типичных сценариев.
Наблюдение за всем объектом целиком
const user = reactive({
name: 'Alex',
age: 30,
preferences: {
theme: 'light'
}
})
// Следим за всем объектом целиком
watch(
() => user,
(newUser) => {
console.log('user изменился', newUser)
},
{ deep: true } // важно для вложенных полей
)
Здесь мы добавили deep: true, чтобы изменения вроде user.preferences.theme = 'dark' вызывали срабатывание колбэка.
Наблюдение за конкретным полем объекта
Чаще всего надежнее и эффективнее следить за конкретным полем с помощью функции:
watch(
() => user.preferences.theme,
(newTheme) => {
console.log('Тема изменилась на', newTheme)
}
)
В этом случае deep не нужен, потому что вы уже указали точное поле.
WatchEffect и объекты
С watchEffect Vue автоматически отслеживает все свойства объекта, которые вы читаете:
watchEffect(() => {
console.log('Имя пользователя', user.name)
console.log('Тема', user.preferences.theme)
})
Если вы позже измените или user.name, или user.preferences.theme, эффект перезапустится.
Асинхронные операции в watch и watchEffect
Важная практическая тема — как правильно делать запросы к API или другие асинхронные действия.
Асинхронный watch с отменой предыдущего запроса
Давайте посмотрим, что происходит в следующем примере: у нас есть строка поиска, и при каждом изменении мы отправляем запрос, при этом предыдущий запрос нужно отменять.
watch(
() => searchQuery.value,
async (query, _oldQuery, onCleanup) => {
if (!query) {
results.value = []
return
}
const controller = new AbortController()
// Регистрируем отмену предыдущего запроса
onCleanup(() => {
controller.abort()
})
try {
const response = await fetch('/api/search?q=' + query, {
signal: controller.signal
})
const data = await response.json()
results.value = data.items
} catch (e) {
// Если запрос отменен, fetch выбросит ошибку,
// ее обычно можно игнорировать
if (e.name !== 'AbortError') {
console.error(e)
}
}
}
)
Обратите внимание, как этот фрагмент кода решает задачу конкурентных запросов: каждый новый ввод пользователя отменяет предыдущий сетевой запрос.
Асинхронный watchEffect
С watchEffect подход похожий, только onCleanup передается как аргумент эффекта.
watchEffect((onCleanup) => {
const query = searchQuery.value
if (!query) {
results.value = []
return
}
const controller = new AbortController()
onCleanup(() => {
controller.abort()
})
fetch('/api/search?q=' + query, {
signal: controller.signal
})
.then(res => res.json())
.then(data => {
results.value = data.items
})
.catch(e => {
if (e.name !== 'AbortError') {
console.error(e)
}
})
})
Разница в том, что источник (searchQuery.value) не указан явно — он определяется во время чтения значения внутри эффекта.
Watch и WatchEffect в Options API
Если вы еще используете Options API, то watch вам уже знаком как опция компонента. Но в Vue 3 шаг за шагом лучше переходить к Composition API. Тем не менее, давайте коротко покажу оба подхода.
watch в Options API
export default {
data() {
return {
count: 0,
user: {
name: 'Alex',
age: 30
}
}
},
watch: {
// Наблюдение за простым полем
count(newVal, oldVal) {
console.log('count изменился', oldVal, '→', newVal)
},
// Глубокое наблюдение за объектом
user: {
handler(newUser) {
console.log('user изменился', newUser)
},
deep: true,
immediate: true
}
}
}
WatchEffect в Options API
watchEffect — это функция Composition API, поэтому в Options API его можно использовать внутри setup.
import { watchEffect, ref } from 'vue'
export default {
setup() {
const count = ref(0)
watchEffect(() => {
console.log('count сейчас', count.value)
})
return { count }
}
}
Типичные ошибки и подводные камни
Теперь давайте посмотрим на несколько проблем, с которыми часто сталкиваются разработчики при работе с watch и watchEffect.
Ошибка 1: использование watch вместо computed
Иногда watch используют там, где нужно обычное вычисляемое значение (computed). Это приводит к лишнему кодy и сложностям.
Плохо:
const count = ref(0)
const doubled = ref(0)
watch(
() => count.value,
(newVal) => {
doubled.value = newVal * 2
},
{ immediate: true }
)
Такой код можно заменить на computed, не используя watch вообще:
const count = ref(0)
const doubled = computed(() => count.value * 2)
Правило: если вы преобразуете данные в другие данные без побочных эффектов, используйте computed, а не watch.
Ошибка 2: глубокий watch без необходимости
Часто включают deep: true «на всякий случай», и это может сильно ударить по производительности.
Вместо:
watch(
settings,
() => {
saveSettings(settings)
},
{ deep: true }
)
Лучше явно указать нужные поля:
watch(
() => ({
theme: settings.theme,
language: settings.language
}),
(val) => {
saveSettings(val)
},
{ deep: true } // здесь deep уже меньше по охвату, чем на всем объекте
)
Еще лучше — выделить конкретные реактивные значения, если это возможно.
Ошибка 3: забытая очистка в асинхронных эффектов
Когда вы используете setInterval, addEventListener или сетевые запросы, важно использовать onCleanup, иначе вы получите утечки памяти или неожиданные эффекты.
watchEffect((onCleanup) => {
const handler = (e: MouseEvent) => {
console.log('Координаты', e.clientX, e.clientY)
}
window.addEventListener('mousemove', handler)
// Обязательно удаляем обработчик при очистке
onCleanup(() => {
window.removeEventListener('mousemove', handler)
})
})
Как выбрать между Watch и WatchEffect на практике
Чтобы вам было проще ориентироваться, можно использовать следующую «мини-стратегию»:
Используйте watchEffect, если:
- вы хотите быстро описать реакцию на несколько значений,
- не нужен
oldValue, - эффект можно считать «легким» и безопасным даже при частых перезапусках,
- вы не против того, что он выполнится сразу при создании.
Примеры:
- логирование,
- синхронизация с localStorage,
- подключение слушателей событий.
Используйте watch, если:
- нужно знать и старое, и новое значение,
- нужно следить только за конкретными полями,
- вы боитесь «лишних» перезапусков и хотите точного контроля,
- важно явно указать, когда запускать колбэк (
immediate,flushи др.).
Примеры:
- отправка запросов к API при изменении ID,
- дорогие вычисления, которые нужно делать только при изменении конкретного поля,
- сложные сценарии с несколькими зависимостями и условиями.
Заключение
Watch и WatchEffect в Vue 3 решают похожую задачу — запуск побочных эффектов при изменении реактивных данных, но делают это разными способами.
watch— явный, контролируемый, с поддержкой старого значения, массивов источников,immediate,deepи других опций.watchEffect— автоматический, простой по синтаксису, мгновенно запускается и сам определяет свои зависимости по факту чтения реактивных значений.
При выборе между ними стоит исходить из того, нужны ли вам старые значения, насколько точный контроль вы хотите иметь над зависимостями и насколько сложен или тяжел ваш побочный эффект.
Если эффект простой и не критичен к частым перезапускам — начинайте с watchEffect. Если нужно точное управление, оптимизация, работа с конкретными полями и учет прошлых значений — переходите к watch.
Частозадаваемые технические вопросы по теме и ответы
Как отменить watcher, созданный через watch или watchEffect
Обе функции возвращают функцию остановки. Давайте посмотрим, что происходит в следующем примере:
const stop = watchEffect(() => {
console.log('слушаю изменения')
})
// Когда нужно перестать следить
stop()
То же самое с watch:
const stop = watch(source, callback)
// ...
stop() // watcher больше не срабатывает
Почему watchEffect срабатывает сразу, а watch — нет
watchEffect по задумке всегда выполняет эффект немедленно, чтобы сразу собрать зависимости. watch по умолчанию срабатывает только при первом изменении. Если нужно поведение «как у watchEffect», включите immediate: true:
watch(source, callback, { immediate: true })
Как использовать watch для отслеживания изменений в массиве
Если массив создан как ref([]), следите за .value или используйте функцию:
const items = ref<string[]>([])
watch(
() => items.value, // отслеживаем ссылку на массив
(newVal) => {
console.log('массив изменился', newVal)
},
{ deep: true } // чтобы ловить изменения элементов
)
Для reactive({ items: [] }) можно использовать () => state.items плюс deep: true.
Можно ли использовать async/await в watchEffect
Да, но аккуратно. Лучше не делать сам watchEffect async, а запускать async-функцию внутри и использовать onCleanup для отмены:
watchEffect((onCleanup) => {
let cancelled = false
onCleanup(() => {
cancelled = true
})
;(async () => {
const data = await loadData()
if (!cancelled) {
result.value = data
}
})()
})
Так вы избегаете неконтролируемых гонок между несколькими запусками эффекта.
Как отдебажить, какие именно зависимости использует watchEffect
Вы можете временно ограничить эффект и логировать все используемые значения:
watchEffect(() => {
console.log('dep1', state.dep1)
console.log('dep2', state.dep2)
})
Если эффект срабатывает слишком часто, попробуйте:
- заменить
watchEffectнаwatchс явными источниками, - убрать чтение лишних полей или обернуть чтения в условия, чтобы они выполнялись реже.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев