Vue Lifecycle Hooks Печать
Добавил(а) microsin   

В процессе времени жизни компонента (Lifecycle) можно перехватывать и обрабатывать отдельные события с помощью специальных функций.

Ниже приведен обзор Lifecycle Hooks (хуков жизненного цикла) в Vue.js 3 (с учётом Composition API) в виде таблицы. Для Vue 2 есть небольшие отличия (например, хук beforeUnmount называется beforeDestroy).

Общая схема жизненного цикла компонента Vue:

Vue Lifecycle Hooks

Сводная таблица хуков жизненного цикла:

Hook (Vue 3) Аналог в Options API (Vue 2/3) Когда вызывается Типичные сценарии использования
onBeforeCreate beforeCreate До инициализации компонента: данные, вычисляемые свойства, методы и события ещё не настроены. Крайне редко, для расширенных плагинов.
onCreated created После инициализации реактивных данных, вычисляемых свойств, методов, но до монтирования DOM (Document Object Model). Запросы к API, инициализация не-DOM данных, работа с событиями.
onBeforeMount beforeMount Перед началом монтирования компонента в реальный DOM. Шаблон уже скомпилирован. Последние операции перед рендерингом (редко).
onMounted mounted После монтирования компонента в DOM. Доступен DOM. Дочерние компоненты также смонтированы. Работа с DOM, инициализация библиотек (карты, графики), таймеры.
onBeforeUpdate beforeUpdate При изменении реактивных данных, но до обновления реального DOM. Получение состояния DOM перед обновлением.
onUpdated updated После обновления DOM из-за изменения реактивных данных. Работа с DOM после обновления, но осторожно — можно вызвать бесконечный цикл.
onBeforeUnmount beforeDestroy (Vue 2) Перед началом размонтирования компонента. Компонент всё ещё в рабочем состоянии. Очистка таймеров, отписка от событий, отмена запросов (prevent memory leaks).
onUnmounted destroyed (Vue 2) После полного размонтирования компонента. Все дочерние компоненты размонтированы. Финальная очистка, уведомление сервера.
onErrorCaptured errorCaptured При поимке ошибки из дочернего компонента. Логирование ошибок, отправка в сервис мониторинга.
onActivated activated При активации компонента, сохранённого в KeepAlive. Повторная подписка на события, обновление данных.
onDeactivated deactivated При деактивации компонента, сохранённого в KeepAlive. Приостановка таймеров, отписка от временных событий.
onServerPrefetch - Только в SSR, перед рендерингом на сервере. Предзагрузка данных для серверного рендеринга.

- Названия хуков размонтирования/уничтожения: в Vue 2 это beforeDestroy и destroyed, в Vue 3 — beforeUnmount и unmounted (что логически точнее).

- Composition API: в Vue 3 хуки нужно явно импортировать и использовать внутри setup() (или < script setup>). В Options API они объявляются как методы компонента.

- Порядок выполнения: общая последовательность (Создание → Монтирование → Обновление → Размонтирование) остаётся неизменной.

Важные предупреждения:

onUpdated: не изменяйте состояние компонента внутри этого хука без условий — это может привести к бесконечному циклу обновлений.
onBeforeUpdate и onUpdated: доступ к DOM в этих хуках может быть небезопасным, если изменения не связаны с текущим компонентом.

Для проектов на Vue 2 основные принципы те же, но названия некоторых хуков отличаются. Эта таблица покрывает ключевые моменты жизненного цикла Vue-компонента.

[Примеры onMounted]

Вот несколько практических примеров использования хука onMounted в Vue 3 (Composition API) и, для сравнения, в Vue 2 (Options API).

1️⃣ Пример: Работа с DOM (обращение к элементам)

Самый частый случай — когда нужно получить доступ к элементу DOM после его рендеринга.

Vue 3 Composition API:

< template>
  < div>
    < h1 ref="titleElement">Привет, Vue 3!< /h1>
    < p>Текст заголовка: {{ titleText }}< /p>
  < /div>
< /template>
< script setup>
import { ref, onMounted } from 'vue'

// Создаём ref для элемента DOM
const titleElement = ref(null)
const titleText = ref('')

onMounted(() => {
// Теперь DOM доступен, элемент гарантированно существует
if (titleElement.value) {
titleText.value = titleElement.value.textContent
console.log('DOM элемент заголовка:', titleElement.value)

// Можем изменять DOM напрямую (хотя лучше использовать реактивность)
titleElement.value.style.color = 'blue'
} }) < /script>

< template>
  < div>
    < h1 ref="titleElement">Привет, Vue 2!< /h1>
    < p>Текст заголовка: {{ titleText }}< /p>
  < /div>
< /template>
< script>
export default {
data() {
return {
titleText: ''
}
}, mounted() {
// Элемент доступен через this.$refs
const element = this.$refs.titleElement
if (element) {
this.titleText = element.textContent
console.log('DOM элемент заголовка:', element)
element.style.color = 'green'
}
} } < /script>

2️⃣ Пример: Загрузка данных с API

Типичный паттерн — выполнение асинхронного запроса при монтировании компонента.

Vue 3 Composition API:

< template>
  < div>
    < h2>Список пользователей< /h2>
    < div v-if="loading">Загрузка...< /div>
    < ul v-else>
      < li v-for="user in users" :key="user.id">{{ user.name }}< /li>
    < /ul>
  < /div>
< /template>
< script setup>
import { ref, onMounted } from 'vue'

const users = ref([])
const loading = ref(true)
onMounted(async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
users.value = await response.json()
} catch (error) {
console.error('Ошибка загрузки:', error)
} finally {
loading.value = false
} }) < /script>

3️⃣ Пример: Инициализация сторонней библиотеки (карта, график, календарь)

Часто требуется инициализировать библиотеку после рендеринга контейнера.

< template>
  < div>
    < div ref="chartContainer" style="width: 600px; height: 400px;">< /div>
  < /div>
< /template>
< script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as echarts from 'echarts' // Пример библиотеки для графиков

const chartContainer = ref(null)
let chartInstance = null
onMounted(() => {
if (chartContainer.value) {
// Инициализируем график только после монтирования DOM элемента
chartInstance = echarts.init(chartContainer.value)
const option = {
title: { text: 'Пример графика' },
xAxis: { data: ['Янв', 'Фев', 'Мар', 'Апр', 'Май'] },
yAxis: {},
series: [{ name: 'Продажи', type: 'bar', data: [5, 20, 36, 10, 15] }]
}

chartInstance.setOption(option)

// Обработчик изменения размера окна
window.addEventListener('resize', handleResize)
} })

// Важно очищать ресурсы onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
window.removeEventListener('resize', handleResize) })

function handleResize() {
if (chartInstance) {
chartInstance.resize()
} } < /script>

4️⃣ Пример: Подписка на события вне Vue

< template>
  < div>
    < p>Ширина окна: {{ windowWidth }}px< /p>
  < /div>
< /template>
< script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const windowWidth = ref(window.innerWidth)
const updateWidth = () => {
windowWidth.value = window.innerWidth }
onMounted(() => {
// Подписываемся на событие только после монтирования
window.addEventListener('resize', updateWidth) })

// Не забываем отписаться при размонтировании onUnmounted(() => {
window.removeEventListener('resize', updateWidth) }) < /script>

Ключевые моменты использования onMounted:

Ситуация Рекомендация
Работа с DOM Используйте ref для получения элементов. onMounted гарантирует, что элемент уже существует в DOM.
Запросы к API Идеальное место для начальной загрузки данных. Управляйте состояниями загрузки и ошибок.
Сторонние библиотеки Инициализируйте библиотеки (карты, графики, редакторы) только после рендеринга контейнера.
Таймеры и подписки Создавайте в onMounted, но обязательно очищайте в onUnmounted (во избежание утечек памяти).
Зависимость от дочерних компонентов Если нужен доступ к дочерним компонентам, они тоже будут смонтированы к моменту вызова onMounted.

Чего НЕ следует делать в onMounted:

- Не изменяйте реактивные данные синхронно без необходимости — это вызовет лишний ререндер.
- Не полагайтесь на onMounted для SSR (серверного рендеринга) — на сервере он не вызывается.
- Не забывайте про очистку в onUnmounted для таймеров, подписок и сторонних библиотек.

Альтернатива с watch + ref: иногда вместо onMounted можно использовать watch с опцией flush: 'post' для реакции на изменение DOM:

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

const chartContainer = ref(null)
watch(chartContainer, (newElement) => {
if (newElement) {
// Элемент появился в DOM (аналог mounted для этого ref)
console.log('Элемент готов:', newElement)
} }, { flush: 'post' }) // 'post' гарантирует выполнение после обновления DOM < /script>

Эти примеры покрывают основные сценарии использования onMounted. Хук особенно важен для любой работы, требующей доступа к реальному DOM-дереву после полного рендеринга компонента.

[Примеры onUpdated]

Вот практические примеры хука onUpdated в Vue 3 (Composition API) с важными предупреждениями.

Критически важное предупреждение: onUpdated вызывается при КАЖДОМ обновлении DOM компонента. Без контроля это приводит к бесконечным циклам или серьёзным проблемам производительности.

1️⃣ Безопасный пример: отслеживание количества обновлений

< template>
  < div>
    < h2>Счётчик: {{ count }}< /h2>
    < button @click="count++">Увеличить< /button>
    < p>Компонент обновлялся: {{ updateCount }} раз< /p>
  < /div>
< /template>
< script setup>
import { ref, onUpdated } from 'vue'

const count = ref(0)
const updateCount = ref(0)

// БЕЗОПАСНО: просто считаем обновления, не меняя реактивные данные onUpdated(() => {
updateCount.value++
console.log(`DOM обновлён. Счётчик: ${count.value}`) }) < /script>

2️⃣ Пример с условием: автосохранение при изменении текста

< template>
  < div>
    < textarea v-model="text" placeholder="Введите текст...">< /textarea>
    < p>Последнее сохранение: {{ lastSaved || 'ещё не было' }}< /p>
  < /div>
< /template>
< script setup>
import { ref, watch, onUpdated } from 'vue'

const text = ref('')
const lastSaved = ref('')
let previousText = ''

// ЛУЧШАЯ ПРАКТИКА: используйте watch для реактивных изменений watch(text, (newText, oldText) => {
// Автосохранение при существенном изменении
if (newText.length > 10 && newText !== oldText) {
simulateSave()
} })

// onUpdated используем только для логирования onUpdated(() => {
console.log('Текущая длина текста:', text.value.length) })

function simulateSave() {
lastSaved.value = new Date().toLocaleTimeString()
console.log('Текст сохранён:', text.value.substring(0, 20) + '...') } < /script>

3️⃣ Пример: интеграция с нативным DOM API (с осторожностью)

< template>
  < div>
    < input ref="inputElement" v-model="message" />
    < p>Сообщение: {{ message }}< /p>
    < p ref="textElement">Длина сообщения: {{ message.length }}< /p>
  < /div>
< /template>
< script setup>
import { ref, onUpdated, nextTick } from 'vue'

const message = ref('')
const inputElement = ref(null)
const textElement = ref(null)

// ВАЖНО: Изменение DOM только при необходимости onUpdated(async () => {
// Используем nextTick для гарантии, что DOM уже обновлён
await nextTick()

// Пример: выделение текста в input при определённых условиях
if (inputElement.value && message.value.length > 15) {
inputElement.value.select()
console.log('Текст выделен (длина > 15)')
}

// Пример: логирование изменения стилей другого элемента
if (textElement.value) {
const color = message.value.length > 10 ? 'darkred' : 'black'
if (textElement.value.style.color !== color) {
textElement.value.style.color = color
}
} }) < /script>

4️⃣ ❌ ОПАСНЫЙ пример (чего НЕ следует делать)

< template>
  < div>{{ value }}< /div>
< /template>
< script setup>
import { ref, onUpdated } from 'vue'

const value = ref(0)

// БЕСКОНЕЧНЫЙ ЦИКЛ: изменение реактивных данных в onUpdated onUpdated(() => {
value.value++ // КРИТИЧЕСКАЯ ОШИБКА: вызовет повторный onUpdated
console.log('Обновление...') // Будет выполняться бесконечно }) < /script>

Сравнение подходов, когда использовать onUpdated, а когда watch:

Ситуация Рекомендуемый подход Почему
Реакция на изменение данных watch или watchEffect Оптимизировано Vue, избегает лишних вызовов
Работа с DOM после обновления onUpdated + nextTick() Гарантирует доступ к обновлённому DOM
Логирование/отладка onUpdated (с осторожностью) Позволяет увидеть все обновления
Побочные эффекты при изменениях watch с условиями Контролируемое выполнение
Интеграция с jQuery/legacy-кодом onUpdated + флаги Но лучше переписать на Vue-методы

✅ Безопасный паттерн использования onUpdated

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

const data = ref(0)
const shouldUpdateDOM = ref(false)

// Основную логику в watch watch(data, (newVal) => {
if (newVal > 5) {
shouldUpdateDOM.value = true
} })

// onUpdated - только для DOM операций onUpdated(async () => {
if (!shouldUpdateDOM.value) return

await nextTick()

// Безопасная работа с DOM
const element = document.getElementById('target')
if (element) {
element.scrollIntoView({ behavior: 'smooth' })
}

shouldUpdateDOM.value = false // Сбрасываем флаг }) < /script>

Золотые правила использования onUpdated:

1. Не изменяйте реактивные данные внутри onUpdated.
2. Используйте условия или флаги для ограничения выполнения.
3. Для данных используйте watch, для DOM - onUpdated.
4. Всегда используйте nextTick() если нужен доступ к обновлённому DOM.
5. Помните о производительности - хук вызывается часто.

Альтернатива: watch с flush: post

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

const count = ref(0)

// Выполнится после обновления DOM (как onUpdated,
// но для конкретного reactive-источника) watch(count, () => {
console.log('Count изменился, DOM обновлён') }, { flush: 'post' }) < /script>

Вывод: onUpdated — мощный, но опасный инструмент. В 95% случаев watch или watchEffect с подходящими опциями — лучшее решение. Используйте onUpdated только для операций, требующих гарантированного доступа к уже обновлённому DOM, и всегда с защитными условиями.

[Примеры onUnmounted]

Вот практические примеры использования хука onUnmounted в Vue 3 (Composition API). Этот хук критически важен для предотвращения утечек памяти.

1️⃣ Базовый пример: очистка таймеров и интервалов

< template>
  < div>
    < h2>Таймер: {{ count }} сек< /h2>
    < button @click="stopTimer" v-if="timer">Остановить таймер< /button>
    < button @click="startTimer" v-else>Запустить таймер< /button>
  < /div>
< /template>
< script setup>
import { ref, onUnmounted } from 'vue'

const count = ref(0)
let timer = null

const startTimer = () => {
timer = setInterval(() => {
count.value++
console.log('Таймер тикает:', count.value)
}, 1000) }

const stopTimer = () => {
if (timer) {
clearInterval(timer)
timer = null
console.log('Таймер остановлен вручную')
} }

// ВАЖНО: очистка при размонтировании компонента onUnmounted(() => {
if (timer) {
clearInterval(timer)
console.log('Таймер очищен в onUnmounted')
} }) < /script>

2️⃣ Пример: отписка от событий и WebSocket

< template>
  < div>
    < p>Ширина окна: {{ windowWidth }}px< /p>
    < p>Сообщения WebSocket: {{ messages.length }}< /p>
  < /div>
< /template>
< script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const windowWidth = ref(window.innerWidth)
const messages = ref([])
let socket = null
let resizeHandler = null
onMounted(() => {
// 1. Подписка на событие resize
resizeHandler = () => {
windowWidth.value = window.innerWidth
}
window.addEventListener('resize', resizeHandler)

// 2. Инициализация WebSocket соединения
socket = new WebSocket('wss://echo.websocket.org')
socket.onmessage = (event) => {
messages.value.push(event.data)
}
socket.onopen = () => {
console.log('WebSocket подключён')
} })

// ОЧИСТКА ВСЕХ РЕСУРСОВ onUnmounted(() => {
// 1. Удаляем обработчик события
if (resizeHandler) {
window.removeEventListener('resize', resizeHandler)
console.log('Обработчик resize удалён')
}

// 2. Закрываем WebSocket соединение
if (socket && socket.readyState === WebSocket.OPEN) {
socket.close()
console.log('WebSocket соединение закрыто')
} }) < /script>

3️⃣ Пример: очистка сторонних библиотек (карты, графики)

< template>
  < div>
    < div ref="mapContainer" style="width: 100%; height: 400px;">< /div>
    < button @click="showMap = !showMap">
      {{ showMap ? 'Удалить карту' : 'Показать карту' }}
    < /button>
  < /div>
< /template>
< script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'

const mapContainer = ref(null)
const showMap = ref(true)
let mapInstance = null
let mapLibrary = null
onMounted(async () => {
if (!showMap.value) return

// Динамический импорт библиотеки (если она тяжёлая)
mapLibrary = await import('leaflet')
await nextTick() // Ждём рендеринг контейнера

if (mapContainer.value) {
mapInstance = mapLibrary.map(mapContainer.value).setView([51.505, -0.09], 13)
mapLibrary.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(mapInstance)
console.log('Карта инициализирована')
} })
onUnmounted(() => {
// Очищаем ресурсы карты
if (mapInstance) {
mapInstance.remove()
mapInstance = null
console.log('Карта удалена из памяти')
}

// Очищаем ссылки на библиотеку
mapLibrary = null }) < /script>

4️⃣ Пример: отмена HTTP-запросов (Axios)

< template>
  < div>
    < h3>Загрузка данных< /h3>
    < button @click="fetchData" :disabled="loading">
      {{ loading ? 'Загрузка...' : 'Загрузить данные' }}
    < /button>
    < div v-if="data">{{ data }}< /div>
  < /div>
< /template>
< script setup>
import { ref, onUnmounted } from 'vue'
import axios from 'axios'

const data = ref(null)
const loading = ref(false)
let abortController = null

const fetchData = async () => {
// Отменяем предыдущий запрос, если он есть
if (abortController) {
abortController.abort()
}

// Создаём новый AbortController
abortController = new AbortController()
loading.value = true

try {
const response = await axios.get('https://jsonplaceholder.typicode.com/todos/1', {
signal: abortController.signal
})
data.value = response.data
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Ошибка загрузки:', error)
}
} finally {
loading.value = false
} }

// Отменяем все pending-запросы при размонтировании onUnmounted(() => {
if (abortController) {
abortController.abort()
console.log('HTTP-запросы отменены')
} }) < /script>

5️⃣ Комплексный пример: компонент с множеством ресурсов

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

const audioElement = ref(null)
let audioContext = null
let animationFrameId = null
let eventListeners = []
onMounted(() => {
// 1. Аудио контекст
audioContext = new (window.AudioContext || window.webkitAudioContext)()

// 2. Запуск анимации
const animate = () => {
// Логика анимации
animationFrameId = requestAnimationFrame(animate)
}
animationFrameId = requestAnimationFrame(animate)

// 3. Несколько обработчиков событий
const clickHandler = () => console.log('Click')
const keyHandler = () => console.log('Key')
document.addEventListener('click', clickHandler)
window.addEventListener('keydown', keyHandler)
eventListeners.push(
{ element: document, type: 'click', handler: clickHandler },
{ element: window, type: 'keydown', handler: keyHandler }
) })

// ЦЕНТРАЛИЗОВАННАЯ ОЧИСТКА onUnmounted(() => {
console.log('Начало очистки ресурсов...')

// 1. Останавливаем аудио контекст
if (audioContext && audioContext.state !== 'closed') {
audioContext.close().then(() => {
console.log('AudioContext закрыт')
})
}

// 2. Отменяем анимацию
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
console.log('Анимация остановлена')
}

// 3. Удаляем все обработчики событий
eventListeners.forEach(({ element, type, handler }) => {
element.removeEventListener(type, handler)
})
eventListeners.length = 0 console.log('Обработчики событий удалены')

// 4. Очищаем DOM ссылки
audioElement.value = null
console.log('Все ресурсы очищены') }) < /script>

Чек-лист: что очищать в onUnmounted:

Тип ресурса Метод очистки Последствия если не очистить
Таймеры/Интервалы clearInterval(), clearTimeout() Выполнение кода после удаления компонента
События DOM removeEventListener() Обработчики продолжают вызываться
WebSocket socket.close() Незакрытые соединения, утечки памяти
HTTP-запросы AbortController.abort() Лишняя нагрузка на сервер/клиент
Сторонние библиотеки .destroy(), .dispose() Утечки памяти, конфликты
Анимации cancelAnimationFrame() Нагрузка на CPU
Подписки RxJS .unsubscribe() Утечки памяти, выполнение логики
ResizeObserver .disconnect() Наблюдение за несуществующими элементами

Паттерны для удобной очистки

< script setup>
import { onUnmounted } from 'vue'

// Паттерн "Коллекционер очистки"
const cleanupCallbacks = []

const registerCleanup = (callback) => {
cleanupCallbacks.push(callback) }

// Регистрируем очистки registerCleanup(() => {
console.log('Первая задача очистки') })
registerCleanup(() => {
console.log('Вторая задача очистки') })

// Выполняем все очистки onUnmounted(() => {
cleanupCallbacks.forEach(callback => callback())
cleanupCallbacks.length = 0 }) < /script>

Распространённые ошибки

< script setup>
// ❌ НЕДОСТАТОЧНАЯ очистка
let timer = setInterval(() => {}, 1000) onUnmounted(() => {
clearInterval(timer) // ОК
// Но timer всё ещё содержит ID, лучше обнулить })

// ✅ ПРАВИЛЬНАЯ очистка
let timer = setInterval(() => {}, 1000) onUnmounted(() => {
clearInterval(timer) timer = null // Явное обнуление })

// ❌ ПРОПУЩЕННАЯ очистка динамических ресурсов onMounted(() => {
// Ресурс создаётся условно
if (Math.random() > 0.5) {
const extraTimer = setInterval(() => {}, 500)
// Забыли сохранить ссылку для очистки!
} })

// ✅ УСЛОВНАЯ очистка
let extraTimer = null onMounted(() => {
if (Math.random() > 0.5) {
extraTimer = setInterval(() => {}, 500)
} }) onUnmounted(() => {
if (extraTimer) {
clearInterval(extraTimer)
} }) < /script>

Ключевые правила:

1. Каждому onMounted — свой onUnmounted.
2. Очищайте всё, что создаётся вручную.
3. Используйте AbortController для fetch/axios.
4. Обнуляйте ссылки после очистки.
5. Тестируйте утечки памяти (Chrome DevTools → Memory).

Правильная очистка в onUnmounted — признак качественного Vue-кода и защита от "зомби-приложений", которые продолжают работать после удаления компонентов.

[watсh: отличие от onUpdated]

watch и onUpdated решают разные задачи, хотя оба реагируют на изменения. Вот ключевые отличия:

Критерий watch / watchEffect onUpdated
Основное назначение Реагировать на изменения конкретных данных (реактивных состояний, props [2]). Выполнить код после любого обновления DOM компонента.
Когда вызывается Сразу после изменения отслеживаемых реактивных данных (до обновления DOM, если не указано flush: 'post'). После каждого обновления виртуального DOM и применения изменений к реальному DOM.
Доступ к DOM По умолчанию нет доступа (DOM может быть устаревшим). С flush: 'post' — есть, но это специализированный случай. Есть (компонент только что обновился).
Производительность Высокая, так как можно точно указать, какие данные отслеживать. Потенциально низкая, хук срабатывает при любом обновлении, без фильтрации.
Риск бесконечного цикла Низкий (если не изменять внутри отслеживаемые данные без условий). Очень высокий, если внутри хука изменить реактивное состояние.
Типичный сценарий Загрузка новых данных при изменении ID, валидация, вычисление производных значений. Интеграция с библиотекой, требующей работы с DOM после обновлений, скроллинг к элементу.

Аргументы watch. По умолчанию функция watch получает значение отслеживаемой переменной уже после того, как она была изменена, через первый параметр (в этом примере отслеживаемая переменная userId):

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

const userId = ref(1)
watch(userId, () => {
console.log(userId.value); }); < /script>

Однако можно передать значение до изменения и после изменения через второй параметр:

watch(userId, (newValue, oldValue) => {
console.log('Старое значение: ' + ${oldValue});
console.log('Новое значение: ' + ${newValue}); });

Набор параметров третьего аргумента watch. Также можно предоставить для watch третий аргумент, который представляет собой набор специальных параметров. Один из них это boolean-флажок immediate. Если он установлен в true, то это заставляет вызвать watch, когда компонент запускается:

watch(  userId, (newValue, oldValue) => {
console.log('Старое значение: ' + ${oldValue});
console.log('Новое значение: ' + ${newValue});
},
{ immediate: true } );

При этом независимо от того, изменился ли userId, или нет, всегда будет один вызов watch.

once. Другой boolean-параметр для watch это once: если он установлен в true, то watch будет вызван только 1 раз.

watch(  userId, (newValue, oldValue) => {
console.log('Старое значение: ' + ${oldValue});
console.log('Новое значение: ' + ${newValue});
},
{ once: true } );

[watchEffect: отслеживание нескольких переменных]

Функция watchEffect не принимает никаких агументов зависимостей, и принимает сразу только callback-функцию. Причина в том, что любое реактивное состояние переменной, которое встретилось в теле этой callback-функции, автоматически становится зависимостью.

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

const userId = ref(1)
const userData = ref(null)
watchEffect(() => {
console.log(userId.value);
console.log(userData.value); }); < /script>

Функция watchEffect позволяет сильно сократить код, когда имеется несколько ослеживаемых зависимостей.

Когда что использовать. Используйте watch (в 90% случаев):

- Реакция на изменение данных (запрос к API при смене фильтра).
- Валидация или вычисление производных значений.
- Выполнение логики при изменении конкретного пропса или состояния.

Используйте onUpdated с осторожностью (в 10% случаев):

- Для отладки (логирование после каждого рендера).
- Для работы со сторонними библиотеками, которым нужен доступ к обновлённому DOM.
- Когда нужно синхронизировать не-Vue состояние с DOM.

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

Пример 1: Загрузка данных при изменении ID (идеально для watch)

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

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

// watch СФОКУСИРОВАН на изменении конкретного reactive-источника watch(userId, async (newId) => {
const response = await fetch(`/api/users/${newId}`)
userData.value = await response.json() }, { immediate: true }) // immediate:true выполнит запрос сразу при создании < /script>

Пример 2: Работа с DOM после обновления (случай для onUpdated)

< template>
  < div>
    < div ref="messageList">
      < p v-for="msg in messages" :key="msg.id">{{ msg.text }}< /p>
    < /div>
  < /div>
< /template>
< script setup>
import { ref, onUpdated, nextTick } from 'vue'

const messages = ref([/*...*/])
const messageList = ref(null)

// onUpdated отслеживает ЛЮБОЕ обновление компонента onUpdated(async () => {
// Ждём следующего тика, чтобы быть уверенным в обновлении DOM
await nextTick()

// Прокручиваем список к низу при добавлении новых сообщений
if (messageList.value) {
messageList.value.scrollTop = messageList.value.scrollHeight
} }) < /script>

Пример 3: антипаттерн — НЕ используйте onUpdated там, где нужен watch

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

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

// ❌ ПЛОХО: onUpdated сработает при ЛЮБОМ обновлении,
// не только при изменении searchQuery onUpdated(async () => {
const response = await fetch(`/api/search?q=${searchQuery.value}`)
searchResults.value = await response.json()
// Беда! Запрос будет выполняться при каждом рендере,
// вызывая бесконечные запросы, если ответ меняет
// какие-то реактивные данные. })

// ✅ ХОРОШО: watch сработает ТОЛЬКО при изменении searchQuery watch(searchQuery, async (newQuery) => {
const response = await fetch(`/api/search?q=${newQuery}`)
searchResults.value = await response.json() }) < /script>

Специальный случай: watch с flush: 'post'. Иногда нужна логика watch, но после обновления DOM. Для этого есть опция flush: 'post':

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

const items = ref([])
const listElement = ref(null)

// watch с flush: 'post' сработает ПОСЛЕ обновления DOM watch(items, () => {
// Теперь можно безопасно работать с DOM
if (listElement.value) {
console.log('Высота списка после обновления:', listElement.value.clientHeight)
} }, { flush: 'post' }) // < -- Ключевая опция < /script>

Итог, простое правило:

watch — следит за данными и реагирует на их изменение.
onUpdated — следит за компонентом и реагирует на его перерисовку.

Практическая рекомендация: если не работаете напрямую с обновлённым DOM, почти всегда выбирайте watch. Он даёт больше контроля, эффективнее и безопаснее. onUpdated — это "тяжёлая артиллерия" для специфических DOM-задач.

[onWatcherCleanup: использование для watch и watchEffect]

onCleanup (также называемый эффектом очистки) — мощный механизм внутри watch и watchEffect для управления побочными эффектами.

Основное назначение. Функция onCleanup регистрирует код, который выполнится перед следующим вызовом эффекта или при остановке вотчера. Это аналог onUnmounted, но для отдельных эффектов.

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

const userId = ref(1) watch(userId, () => {
console.log(userId.value);
onWatcherCleanup(() => {
console.log("Очистка");
}) }); < /script>

Сравнение с onUnmounted:

 

Критерий onCleanup (в watch/watchEffect) onUnmounted (хук жизненного цикла)
Область действия Конкретный эффект внутри watch/watchEffect Весь компонент
Время выполнения 1. Перед повторным выполнением эффекта
2. При остановке вотчера (размонтировании)
Только при размонтировании компонента
Доступ к данным Имеет доступ к замыканию текущего эффекта Имеет доступ ко всему компоненту
Идеальный сценарий Отмена запроса, очистка таймера, отписка от события для конкретного отслеживания Глобальная очистка всех ресурсов компонента

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

1️⃣ Отмена предыдущего HTTP-запроса (Debounce + AbortController). Самый частый и важный паттерн:

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

const searchQuery = ref('')
const searchResults = ref([])
const isLoading = ref(false)
watch(searchQuery, (newQuery, oldQuery, onCleanup) => {
if (!newQuery.trim()) {
searchResults.value = []
return
}

isLoading.value = true
const abortController = new AbortController()

// Регистрируем функцию очистки onCleanup(() => {
abortController.abort() // Отменяем предыдущий запрос
console.log('Запрос отменён:', oldQuery)
})

// Выполняем запрос с сигналом отмены
fetch(`https://api.example.com/search?q=${newQuery}`, {
signal: abortController.signal
})
.then(response => response.json())
.then(data => {
searchResults.value = data
})
.catch(err => {
if (err.name !== 'AbortError') {
console.error('Ошибка запроса:', err)
}
})
.finally(() => {
isLoading.value = false
}) }, { debounce: 300 }) // Опционально: добавляем debounce < /script>

2️⃣ Управление таймерами и интервалами

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

const count = ref(0)
const isActive = ref(true)

// watchEffect автоматически отслеживает все используемые reactive-значения watchEffect((onCleanup) => {
if (!isActive.value) return

const intervalId = setInterval(() => {
count.value++
console.log('Счётчик:', count.value)
}, 1000)

// Очистка при изменении isActive или остановке эффекта
onCleanup(() => {
clearInterval(intervalId)
console.log('Интервал очищен')
}) }) < /script>

3️⃣ Подписка и отписка от событий сторонних библиотек

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

const chartData = ref({ /* ... */ })
let chartInstance = null

// Отслеживаем изменения данных для обновления графика watch(chartData, (newData, oldData, onCleanup) => {
if (!chartInstance) {
chartInstance = initChartLibrary('#chart-container')
}

// Подписываемся на событие клика по графику
const clickHandler = (event) => {
console.log('Клик по точке:', event.point)
}

chartInstance.on('click', clickHandler)

// Отписываемся перед следующим обновлением
onCleanup(() => {
chartInstance.off('click', clickHandler)
})
chartInstance.updateData(newData) }, { deep: true }) < /script>

4️⃣ Управление соединениями WebSocket/RxJS

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

const userId = ref(null)
let socket = null
watch(userId, (newId, oldId, onCleanup) => {
if (!newId) return

// Создаём новое WebSocket соединение для пользователя
socket = new WebSocket(`wss://api.example.com/ws?user=${newId}`)

socket.onmessage = (event) => {
console.log('Сообщение:', event.data)
}

// Закрываем соединение при изменении userId или остановке
onCleanup(() => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.close(1000, 'UserId changed')
console.log(`Соединение для пользователя ${oldId} закрыто`)
}
socket = null
}) }) < /script>

5️⃣ Сложный пример: комбинированная очистка

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

const settings = ref({
pollInterval: 5000,
autoRefresh: true })

// Динамически меняем логику в зависимости от настроек watchEffect((onCleanup) => {
const cleanupCallbacks = []

if (settings.value.autoRefresh) {
// 1. Таймер опроса
const pollTimer = setInterval(() => {
fetchData()
}, settings.value.pollInterval)

cleanupCallbacks.push(() => {
clearInterval(pollTimer)
console.log('Таймер опроса очищен')
})

// 2. Подписка на события документа
const clickHandler = () => console.log('Document clicked')
document.addEventListener('click', clickHandler)

cleanupCallbacks.push(() => {
document.removeEventListener('click', clickHandler)
console.log('Подписка на события очищена')
})
}

// Общая функция очистки для всех ресурсов
onCleanup(() => {
cleanupCallbacks.forEach(cb => cb())
cleanupCallbacks.length = 0
}) })

async function fetchData() {
// Логика загрузки данных } < /script>

Порядок выполнения очистки:

watchEffect((onCleanup) => {
// 1. Первый запуск эффекта
console.log('Эффект выполняется')

onCleanup(() => {
// 3. Выполнится ПЕРЕД следующим запуском эффекта
console.log('Очистка предыдущего эффекта')
})

// 2. Основная логика эффекта })

// При изменении зависимостей:
// 1. Выполняется функция очистки (если была зарегистрирована)
// 2. Выполняется эффект снова

Важные нюансы:

1. Синхронная регистрация

// ✅ ПРАВИЛЬНО: onCleanup регистрируется синхронно
watchEffect((onCleanup) => {
const timer = setTimeout(() => {}, 1000)
onCleanup(() => clearTimeout(timer)) // Синхронно! })

// ❌ ОШИБКА: onCleanup вызывается асинхронно watchEffect(async (onCleanup) => {
const data = await fetchData()
// Уже поздно! Эффект мог перезапуститься до этого момента
onCleanup(() => { /* ... */ }) // Не сработает как ожидается })

2. Несколько вызовов onCleanup

watchEffect((onCleanup) => {
// Можно регистрировать несколько функций очистки
onCleanup(() => console.log('Очистка 1'))
onCleanup(() => console.log('Очистка 2'))

// Они выполнятся в порядке, обратном регистрации:
// 1. "Очистка 2"
// 2. "Очистка 1" })

3. Остановка вотчера вручную

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

const stop = watchEffect((onCleanup) => {
onCleanup(() => {
console.log('Эффект остановлен')
}) })

// Остановка вручную вызовет функцию очистки onUnmounted(() => {
stop() }) < /script>

Итоговая таблица применения:

Сценарий Реализация с onCleanup Альтернатива без onCleanup
Отмена запроса при новом AbortController + onCleanup Глобальный AbortController в компоненте
Таймеры/интервалы clearInterval в onCleanup Хранение ID в ref и очистка в onUnmounted
Подписки на события Отписка в onCleanup Массив подписок и очистка в onUnmounted
Временные ресурсы Локальная очистка Глобальная очистка, усложнение кода

Когда использовать onCleanup:

1. Эффект создаёт ресурсы, которые живут только до следующего вызова.
2. Нужно отменять предыдущие операции (запросы, таймеры).
3. Подписки/отписки на события, связанные с конкретными данными.
4. Временная интеграция со сторонними библиотеками.

Ключевое преимущество: onCleanup инкапсулирует логику очистки внутри самого эффекта, делая код более модульным и предотвращая утечки памяти.

[Ссылки]

1. Vue 3, быстрый старт.
2Vue defineProps.