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

11. Lifecycle Hooks

Каждый компонент Vue проходит через несколько этапов жизни: создание, монтирование в DOM, обновление при изменении данных и уничтожение. На каждом этапе можно “подцепиться” к нужному моменту с помощью lifecycle hooks. Это как подписка на события жизни компонента! 🎭


Создание экземпляра
setup() ← Composition API запускается здесь
onBeforeMount()
Создание VDOM → монтирование в реальный DOM
onMounted() ✅ ← DOM доступен!
┌──────┴──────┐
│ Реактивные │
│ данные │
│ изменились │
└──────┬──────┘
onBeforeUpdate()
Патч DOM (только изменения)
onUpdated()
<KeepAlive> deactivated/activated
Компонент удаляется
onBeforeUnmount()
Очистка слушателей, таймеров, подписок
onUnmounted() ✅ ← DOM удалён

onBeforeMount — компонент готов к рендеру, но ещё не вставлен в DOM. onMounted — DOM уже есть, можно работать с реальными элементами:

<script setup lang="ts">
import {
ref,
onBeforeMount,
onMounted
} from 'vue'
const title = ref('Загрузка...')
const canvasRef = ref<HTMLCanvasElement | null>(null)
const users = ref<string[]>([])
onBeforeMount(() => {
// DOM ещё нет! el === null
console.log('onBeforeMount: готовимся к рендеру')
// Можно подготовить данные
title.value = 'Компонент инициализирован'
})
onMounted(async () => {
// DOM уже есть! Можно работать с canvasRef.value
console.log('onMounted: DOM доступен 🎉')
// 1. Работа с DOM-элементами
if (canvasRef.value) {
const ctx = canvasRef.value.getContext('2d')
ctx?.fillRect(0, 0, 100, 100)
}
// 2. Запросы к API
const response = await fetch('/api/users')
users.value = await response.json()
// 3. Инициализация сторонних библиотек
// new Chart(canvasRef.value, {...})
})
</script>
<template>
<h1>{{ title }}</h1>
<canvas ref="canvasRef" width="300" height="200" />
<ul>
<li v-for="user in users" :key="user">{{ user }}</li>
</ul>
</template>

💡 Важно: onMounted — самый частый хук. Используй его для запросов к API, инициализации библиотек (Chart.js, Leaflet, GSAP) и работы с DOM.


Эти хуки срабатывают при каждом обновлении компонента из-за изменений реактивных данных:

<script setup lang="ts">
import { ref, onBeforeUpdate, onUpdated } from 'vue'
const count = ref(0)
const items = ref(['Яблоко', 'Банан'])
const listRef = ref<HTMLUListElement | null>(null)
onBeforeUpdate(() => {
// DOM ещё старый!
console.log('onBeforeUpdate: сейчас DOM обновится')
// Например, можно сохранить позицию скролла перед обновлением
const scrollPos = listRef.value?.scrollTop
console.log('Текущий скролл:', scrollPos)
})
onUpdated(() => {
// DOM уже обновлён!
console.log('onUpdated: DOM синхронизирован ✅')
// Пример: автоскролл к последнему элементу
if (listRef.value) {
listRef.value.scrollTop = listRef.value.scrollHeight
}
})
</script>
<template>
<button @click="items.push('Элемент ' + (items.length + 1))">
Добавить
</button>
<ul ref="listRef" style="max-height: 100px; overflow-y: auto">
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</template>

⚠️ Осторожно: Не изменяй реактивные данные в onUpdated — это вызовет бесконечный цикл обновлений!


Это самые важные хуки для очистки ресурсов. Без очистки — утечки памяти 🕳️:

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
const position = ref({ x: 0, y: 0 })
let animationFrameId: number
let intervalId: ReturnType<typeof setInterval>
let socket: WebSocket
// Мышь
function handleMouseMove(e: MouseEvent) {
position.value = { x: e.clientX, y: e.clientY }
}
onMounted(() => {
// Подписки, которые нужно очистить
window.addEventListener('mousemove', handleMouseMove)
intervalId = setInterval(() => {
console.log('Тик...')
}, 1000)
socket = new WebSocket('wss://example.com')
// Анимационный цикл
const animate = () => {
// ... логика
animationFrameId = requestAnimationFrame(animate)
}
animationFrameId = requestAnimationFrame(animate)
})
onBeforeUnmount(() => {
// Вызывается ДО удаления из DOM
// Ещё можно работать с DOM-элементами
console.log('onBeforeUnmount: компонент скоро исчезнет')
})
onUnmounted(() => {
// Вызывается ПОСЛЕ удаления из DOM
// Обязательная очистка!
window.removeEventListener('mousemove', handleMouseMove)
clearInterval(intervalId)
cancelAnimationFrame(animationFrameId)
socket.close()
console.log('onUnmounted: всё почищено ✅')
})
</script>

Хук для обработки ошибок дочерних компонентов. Работает как Error Boundary в React:

<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue'
interface ErrorInfo {
message: string
component: string
type: string
}
const error = ref<ErrorInfo | null>(null)
onErrorCaptured((err: Error, instance, info) => {
console.error('Перехвачена ошибка:', err)
error.value = {
message: err.message,
component: instance?.$options.name || 'Unknown',
type: info, // 'render', 'setup', 'watch', etc.
}
// Вернуть false — ошибка не распространяется выше
// Вернуть true (или ничего) — ошибка продолжает всплывать
return false
})
</script>
<template>
<div>
<div v-if="error" class="error-boundary">
<h2>⚠️ Что-то пошло не так</h2>
<p>{{ error.message }}</p>
<button @click="error = null">Попробовать снова</button>
</div>
<slot v-else />
</div>
</template>

Когда компонент обёрнут в <KeepAlive>, он не уничтожается при скрытии — а деактивируется. Для таких компонентов есть специальные хуки:

<!-- Родительский компонент -->
<template>
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
</template>
<!-- Компонент внутри KeepAlive -->
<script setup lang="ts">
import { ref, onActivated, onDeactivated, onMounted, onUnmounted } from 'vue'
const lastActivated = ref<Date | null>(null)
let pollingInterval: ReturnType<typeof setInterval>
// Вызывается ОДИН РАЗ при первом монтировании
onMounted(() => {
console.log('onMounted: компонент создан')
})
// Вызывается каждый раз при ПОКАЗЕ компонента
onActivated(() => {
console.log('onActivated: компонент снова активен')
lastActivated.value = new Date()
// Возобновляем polling данных
pollingInterval = setInterval(fetchData, 5000)
})
// Вызывается при СКРЫТИИ (но не удалении!)
onDeactivated(() => {
console.log('onDeactivated: компонент скрыт')
clearInterval(pollingInterval)
})
// Вызывается при полном УДАЛЕНИИ (если KeepAlive убран)
onUnmounted(() => {
console.log('onUnmounted: компонент удалён совсем')
})
async function fetchData() {
// Обновление данных...
}
</script>

Для серверного рендеринга (Nuxt и т.п.) есть специальный хук для предзагрузки данных:

<script setup lang="ts">
import { ref, onServerPrefetch } from 'vue'
import { useAsyncData } from '#imports' // Nuxt
const posts = ref([])
// Выполняется только на сервере перед рендером
onServerPrefetch(async () => {
posts.value = await fetch('https://api.example.com/posts')
.then(r => r.json())
// Данные будут вставлены в HTML до отправки клиенту
})
</script>

<!-- Options API (старый стиль) -->
<script>
export default {
// НЕТ beforeCreate/created — setup() их заменяет
beforeCreate() {},
created() {}, // ← в Composition API = просто код в setup()
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeUnmount() {},
unmounted() {},
errorCaptured() {},
activated() {},
deactivated() {},
serverPrefetch() {},
}
</script>
<!-- Composition API (новый стиль) -->
<script setup lang="ts">
import {
onBeforeMount, // beforeMount
onMounted, // mounted
onBeforeUpdate, // beforeUpdate
onUpdated, // updated
onBeforeUnmount, // beforeUnmount
onUnmounted, // unmounted
onErrorCaptured, // errorCaptured
onActivated, // activated
onDeactivated, // deactivated
onServerPrefetch, // serverPrefetch
} from 'vue'
// beforeCreate и created — не нужны!
// Любой код в setup() = created()
const msg = 'Это выполняется как created'
</script>

В Composition API можно вызвать один хук несколько раз! Все обработчики выполнятся по порядку:

<script setup lang="ts">
import { onMounted } from 'vue'
// Это удобно, когда логика в разных composables
onMounted(() => {
console.log('1. Первый обработчик')
})
onMounted(() => {
console.log('2. Второй обработчик')
})
// Из composable:
// useFetchData() внутри тоже вызывает onMounted
// и это работает!

Реальный пример: компонент с полным циклом

Заголовок раздела «Реальный пример: компонент с полным циклом»
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
interface Post {
id: number
title: string
body: string
}
const posts = ref<Post[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
let abortController: AbortController
async function fetchPosts() {
abortController = new AbortController()
loading.value = true
error.value = null
try {
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts?_limit=5',
{ signal: abortController.signal }
)
posts.value = await response.json()
} catch (e) {
if (e instanceof Error && e.name !== 'AbortError') {
error.value = e.message
}
} finally {
loading.value = false
}
}
onMounted(() => {
fetchPosts()
})
onActivated(() => {
// Обновляем данные при повторном показе
fetchPosts()
})
onBeforeUnmount(() => {
// Отменяем незавершённые запросы
abortController?.abort()
})
</script>
<template>
<div>
<div v-if="loading">⏳ Загрузка...</div>
<div v-else-if="error">❌ {{ error }}</div>
<ul v-else>
<li v-for="post in posts" :key="post.id">
{{ post.title }}
</li>
</ul>
</div>
</template>

ХукКогдаДоступ к DOMТипичное использование
onBeforeMountДо вставки в DOMПодготовка данных
onMountedПосле вставки в DOMAPI запросы, DOM-библиотеки
onBeforeUpdateДо патча DOM✅ (старый)Сохранение состояния
onUpdatedПосле патча DOM✅ (новый)Реакция на DOM-изменения
onBeforeUnmountДо удаленияПоследние действия
onUnmountedПосле удаленияОчистка ресурсов
onErrorCapturedПри ошибке дочернегоError boundary
onActivatedKeepAlive: показВозобновление polling
onDeactivatedKeepAlive: скрытиеПауза polling
onServerPrefetchSSR до рендераПредзагрузка данных