← Курс/watch() и watchEffect() в Vue 3#204 из 257+30 XP

watch() и watchEffect() в Vue 3

Зачем нужны watchers

**Computed** отлично подходит для производных данных. Но иногда при изменении данных нужно выполнить **побочный эффект**: отправить запрос на сервер, записать в localStorage, обновить заголовок страницы и т.д. Для этого используются watchers.

watch() — явное отслеживание

watch() наблюдает за конкретным источником данных:

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

const searchQuery = ref('')
const results = ref([])

// Отслеживаем конкретный ref
watch(searchQuery, async (newValue, oldValue) => {
  console.log(`Поиск изменился: "${oldValue}" -> "${newValue}"`)
  if (newValue.length >= 2) {
    results.value = await fetchResults(newValue)
  } else {
    results.value = []
  }
})
</script>

Опции watch

// immediate: true — запустить немедленно (не ждать первого изменения)
watch(searchQuery, (newVal) => {
  console.log('Запрос:', newVal)
}, { immediate: true })

// deep: true — отслеживать вложенные изменения в объекте
const user = ref({ name: 'Алексей', address: { city: 'Москва' } })

watch(user, (newVal) => {
  console.log('Пользователь изменился:', newVal)
}, { deep: true })

// Без deep: true изменение user.value.address.city не вызовет watch

// once: true (Vue 3.4+) — сработает только один раз
watch(searchQuery, (newVal) => {
  console.log('Первое изменение:', newVal)
}, { once: true })

Наблюдение за несколькими источниками

import { ref, watch } from 'vue'

const firstName = ref('Алексей')
const lastName = ref('Иванов')

// Массив источников
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  console.log(`Имя: ${oldFirst} -> ${newFirst}`)
  console.log(`Фамилия: ${oldLast} -> ${newLast}`)
})

Наблюдение за свойством объекта

const user = reactive({ name: 'Алексей', age: 25 })

// Нельзя так: watch(user.name, ...) — потеряем реактивность
// Нужно использовать getter-функцию:
watch(() => user.name, (newName) => {
  console.log('Имя изменилось:', newName)
})

watchEffect() — автоматическое отслеживание

watchEffect() сам определяет зависимости — отслеживает все реактивные значения, к которым обратились внутри функции:

import { ref, watchEffect } from 'vue'

const userId = ref(1)
const userData = ref(null)

// Запускается немедленно и при изменении любой зависимости
watchEffect(async () => {
  // Vue автоматически отследит, что мы читаем userId.value
  const data = await fetch(`/api/users/${userId.value}`)
  userData.value = await data.json()
})

// При изменении userId запрос повторится автоматически
userId.value = 2

Остановка watcher

Обе функции возвращают функцию остановки:

const stop = watch(count, (newVal) => {
  console.log(newVal)
})

// Позже — остановить наблюдение
stop()

const stopEffect = watchEffect(() => {
  console.log(count.value)
})
stopEffect()

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

| | watch | watchEffect | computed |

|---|---|---|---|

| Зависимости | Явные | Автоматические | Автоматические |

| Первый запуск | При изменении (или immediate) | Сразу | При чтении |

| Доступ к старому значению | Да | Нет | Нет |

| Возвращает значение | Нет | Нет | Да |

| Когда использовать | Async, side effects | Отслеживание нескольких | Производные данные |

Примеры

Реализация простого watch через Proxy — перехват изменений и вызов колбэков

// Реализуем систему наблюдателей на основе Proxy.
// При изменении любого свойства объекта вызываются
// соответствующие обработчики.

function createReactive(obj) {
  // Map: ключ свойства -> массив колбэков
  const watchers = new Map()

  const proxy = new Proxy(obj, {
    set(target, key, newValue) {
      const oldValue = target[key]
      target[key] = newValue

      // Вызываем все watchers для этого свойства
      if (watchers.has(key) && newValue !== oldValue) {
        watchers.get(key).forEach(fn => fn(newValue, oldValue))
      }

      // Вызываем watchers для '*' (наблюдение за любыми изменениями)
      if (watchers.has('*')) {
        watchers.get('*').forEach(fn => fn(key, newValue, oldValue))
      }

      return true
    }
  })

  // Метод watch возвращает функцию отписки
  function watch(key, callback, options = {}) {
    if (!watchers.has(key)) {
      watchers.set(key, [])
    }
    watchers.get(key).push(callback)

    // immediate: true — запустить немедленно
    if (options.immediate) {
      callback(proxy[key], undefined)
    }

    // Возвращаем функцию остановки
    return function stop() {
      const handlers = watchers.get(key)
      const index = handlers.indexOf(callback)
      if (index > -1) handlers.splice(index, 1)
    }
  }

  return { proxy, watch }
}

// --- Демонстрация ---

const { proxy: state, watch } = createReactive({
  count: 0,
  name: 'Vue',
  loading: false
})

// Наблюдаем за count
const stopCount = watch('count', (newVal, oldVal) => {
  console.log(`count: ${oldVal} -> ${newVal}`)
})

// Наблюдаем за name с immediate
watch('name', (newVal, oldVal) => {
  console.log(`name: ${oldVal ?? 'нет'} -> ${newVal}`)
}, { immediate: true })

// Наблюдаем за всеми изменениями
watch('*', (key, newVal, oldVal) => {
  console.log(`[any] ${String(key)}: ${oldVal} -> ${newVal}`)
})

console.log('\n--- Изменения ---')
state.count = 1   // вызовет watch('count') и watch('*')
state.count = 2
state.name = 'Vue 3'  // вызовет watch('name') и watch('*')

console.log('\n--- Остановка watcher count ---')
stopCount()
state.count = 3   // watch('count') уже не вызовется, но watch('*') — да