Skip to content

watchEffect и системы отслеживания в Vue 3

1. Что такое watchEffect

watchEffect - это функция в Composition API Vue 3, которая отслеживает зависимости внутри переданного ей колбэка и автоматически перезапускает его при изменении любой из этих зависимостей. Это мощный инструмент для реактивных вычислений и побочных эффектов.

2. Основы watchEffect

Базовое использование

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

const count = ref(0);
const doubled = ref(0);

// watchEffect автоматически отслеживает зависимости
watchEffect(() => {
  // Колбэк запустится при изменении count
  doubled.value = count.value * 2;
  console.log(`Значение: ${count.value}, удвоенное значение: ${doubled.value}`);
});

function increment() {
  count.value++;
}
</script>

<template>
  <div>
    <p>Счётчик: {{ count }}</p>
    <p>Удвоенное значение: {{ doubled }}</p>
    <button @click="increment">Увеличить</button>
  </div>
</template>

Особенности выполнения

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

const count = ref(0);

// watchEffect запускается немедленно при создании
watchEffect(() => {
  console.log(`Текущее значение: ${count.value}`);
  // Автоматически отслеживает count
});

// Будет выведено "Текущее значение: 0" сразу после создания
</script>

3. Параметры watchEffect

Тайминг выполнения (flush)

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

const count = ref(0);
const element = ref(null);

// По умолчанию (pre) - запускается до обновления DOM
watchEffect(() => {
  console.log(`count изменился на: ${count.value}`);
});

// Post - запускается после обновления DOM
watchEffect(() => {
  if (element.value) {
    // DOM уже обновлен, работа с элементом безопасна
    element.value.textContent = `Значение: ${count.value}`;
  }
}, { flush: 'post' });

// Sync - запускается синхронно при изменении реактивного значения
watchEffect(() => {
  console.log(`Синхронно: ${count.value}`);
}, { flush: 'sync' });
</script>

<template>
  <div>
    <p ref="element"></p>
    <button @click="count++">Увеличить ({{ count }})</button>
  </div>
</template>

Остановка наблюдателя

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

const count = ref(0);

// watchEffect возвращает функцию остановки
const stop = watchEffect(() => {
  console.log(`Текущее значение: ${count.value}`);
});

// Остановить наблюдатель после 5 секунд
setTimeout(() => {
  stop();
  console.log('Наблюдатель остановлен');
  
  // Дальнейшие изменения count не приведут к вызову колбэка
  count.value++;
}, 5000);
</script>

4. Очистка побочных эффектов

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

const id = ref(1);
const data = ref(null);

watchEffect((onCleanup) => {
  // Представим, что это запрос к API
  const controller = new AbortController();
  const { signal } = controller;
  
  // Запускаем асинхронную операцию
  fetchData(id.value, signal).then(result => {
    data.value = result;
  });
  
  // Регистрируем функцию очистки
  onCleanup(() => {
    // Отменяем запрос при изменении id или уничтожении компонента
    controller.abort();
    console.log('Запрос отменен');
  });
});

async function fetchData(id, signal) {
  // Имитация API-запроса
  return fetch(`https://api.example.com/data/${id}`, { signal })
    .then(response => response.json());
}
</script>

5. Сравнение с watch

watch - явное указание источников

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

const firstName = ref('John');
const lastName = ref('Doe');
const fullName = ref('');

// Явно указываем, что отслеживаем firstName и lastName
watch([firstName, lastName], ([newFirstName, newLastName]) => {
  fullName.value = `${newFirstName} ${newLastName}`;
});

// Колбэк вызывается только при изменении указанных источников
</script>

watchEffect - автоматическое отслеживание

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

const firstName = ref('John');
const lastName = ref('Doe');
const fullName = ref('');

// Автоматически отслеживает все используемые реактивные значения
watchEffect(() => {
  fullName.value = `${firstName.value} ${lastName.value}`;
});

// Колбэк вызывается сразу при создании и при любом изменении зависимостей
</script>

Доступ к старым значениям

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

const count = ref(0);

// watch предоставляет доступ к старым и новым значениям
watch(count, (newValue, oldValue) => {
  console.log(`Изменено с ${oldValue} на ${newValue}`);
});

// watchEffect не имеет доступа к предыдущим значениям
watchEffect(() => {
  // Доступно только текущее значение
  console.log(`Текущее значение: ${count.value}`);
});
</script>

6. Глубокое отслеживание объектов

watchEffect с вложенными свойствами

vue
<script setup>
import { ref, reactive, watchEffect } from 'vue';

const user = reactive({
  name: 'John',
  address: {
    city: 'New York',
    country: 'USA'
  }
});

// Автоматически отслеживает все используемые свойства, включая вложенные
watchEffect(() => {
  console.log(`${user.name} живет в ${user.address.city}, ${user.address.country}`);
});

// При изменении любого из используемых свойств, колбэк будет перезапущен
setTimeout(() => {
  user.address.city = 'Boston';
}, 2000);
</script>

Проблемы с новыми свойствами

vue
<script setup>
import { reactive, watchEffect } from 'vue';

const user = reactive({
  name: 'John'
});

watchEffect(() => {
  // Это свойство существует при первом запуске
  console.log(`Имя: ${user.name}`);
  
  // Это свойство еще не существует при первом запуске
  if (user.age) {
    console.log(`Возраст: ${user.age}`);
  }
});

// Добавление нового свойства
setTimeout(() => {
  user.age = 30;
  // НЕ вызовет перезапуск watchEffect, если это свойство
  // не использовалось при первоначальном запуске
}, 1000);
</script>

7. Практические примеры

Синхронизация с LocalStorage

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

// Загрузка начального значения из localStorage
const theme = ref(localStorage.getItem('theme') || 'light');

// Автоматическая синхронизация с localStorage
watchEffect(() => {
  localStorage.setItem('theme', theme.value);
  document.body.className = theme.value;
});

function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light';
}
</script>

<template>
  <button @click="toggleTheme">
    Переключить тему ({{ theme }})
  </button>
</template>

Условное отслеживание

vue
<script setup>
import { ref, computed, watchEffect } from 'vue';

const isActive = ref(false);
const count = ref(0);

// Создаем вычисляемое свойство для условного отслеживания
const activeCount = computed(() => {
  // Возвращаем count только если isActive === true
  return isActive.value ? count.value : null;
});

// Теперь watchEffect будет реагировать на изменения count
// только когда isActive === true
watchEffect(() => {
  const value = activeCount.value;
  if (value !== null) {
    console.log(`Активный счетчик: ${value}`);
  }
});

function increment() {
  count.value++;
}
</script>

<template>
  <div>
    <button @click="isActive = !isActive">
      {{ isActive ? 'Деактивировать' : 'Активировать' }} отслеживание
    </button>
    <button @click="increment">Увеличить ({{ count }})</button>
  </div>
</template>

Дебаунс для API-запросов

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

const searchQuery = ref('');
const searchResults = ref([]);
const isLoading = ref(false);

// Функция для создания дебаунсера
function debounce(fn, delay) {
  let timeout;
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args), delay);
  };
}

// Создаем дебаунсированную функцию поиска
const debouncedSearch = debounce(async (query) => {
  if (!query.trim()) {
    searchResults.value = [];
    return;
  }
  
  isLoading.value = true;
  try {
    const response = await fetch(`https://api.example.com/search?q=${query}`);
    searchResults.value = await response.json();
  } catch (error) {
    console.error('Ошибка поиска:', error);
    searchResults.value = [];
  } finally {
    isLoading.value = false;
  }
}, 300);

// Используем watchEffect для отслеживания изменений поискового запроса
watchEffect(() => {
  const query = searchQuery.value;
  debouncedSearch(query);
});
</script>

<template>
  <div>
    <input v-model="searchQuery" placeholder="Поиск..." />
    <div v-if="isLoading">Загрузка...</div>
    <ul v-else>
      <li v-for="(result, index) in searchResults" :key="index">
        {{ result.title }}
      </li>
    </ul>
  </div>
</template>

8. Типизация с TypeScript

vue
<script setup lang="ts">
import { ref, watchEffect } from 'vue';

interface User {
  id: number;
  name: string;
  email: string;
}

const user = ref<User | null>(null);

// Типизированная функция асинхронного запроса
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`https://api.example.com/users/${id}`);
  return response.json();
}

// watchEffect с обработкой ошибок
watchEffect(async (onCleanup) => {
  if (!user.value) {
    try {
      const controller = new AbortController();
      onCleanup(() => controller.abort());
      
      const fetchedUser = await fetchUser(1);
      user.value = fetchedUser;
    } catch (error) {
      console.error('Ошибка при загрузке пользователя:', error);
    }
  }
});
</script>

watchEffect - это мощный инструмент для реактивного программирования в Vue 3, который позволяет автоматически отслеживать зависимости и реагировать на их изменения. В отличие от watch, который требует явного указания отслеживаемых источников, watchEffect определяет зависимости динамически во время выполнения, что делает код более лаконичным и гибким.