Компоненти Vue та їх життєві цикли
Життєвий цикл компонента Vue (Vue component lifecycle) - це впорядковані фази, через які проходить кожен компонент від створення до видалення, з хуками для виконання коду на кожній з них.
Теорія
TL;DR
- Аналогія: зміна офіціанта в ресторані - найнятий (
beforeCreate/created), вийшов на роботу (beforeMount/mounted), обслуговує столи (beforeUpdate/updated), пішов додому (beforeUnmount/unmounted) - Options API:
mounted; Composition API:onMounted- той самий момент, різний синтаксис - DOM не існує до
mounted/onMounted- звернення до$refsуcreatedповерне undefined - Хуки - для побічних ефектів (API, таймери, DOM); для реактивної логіки -
watch/watchEffect - SSR пропускає
beforeMount/mountedповністю - спільний код сервер/клієнт розміщуй уcreated/setup
Швидкий приклад
// Options API: порядок хуків при монтуванні і знищенні
export default {
name: 'LifecycleDemo',
data() { return { count: 0 }; },
beforeCreate() { console.log('1. beforeCreate - дані не готові:', this.count); }, // undefined
created() { console.log('2. created - дані готові:', this.count); }, // 0
beforeMount() { console.log('3. beforeMount - DOM відсутній'); },
mounted() { console.log('4. mounted - DOM доступний'); },
beforeUnmount(){ console.log('5. beforeUnmount - очищення тут'); },
unmounted() { console.log('6. unmounted'); }
};
// Консоль: 1 → 2 → 3 → 4 ... потім при знищенні: 5 → 6data() ініціалізується між beforeCreate і created. DOM доступний тільки починаючи з mounted.
Options API проти Composition API
Хуки Options API (beforeCreate, mounted) оголошуються прямо в об'єкті компонента, як методи класу. Просто і зрозуміло, але логіка розкидана по різних секціях одного файлу.
Composition API загортає ту саму логіку в setup() через функції onMounted, onBeforeUnmount та інші. Прямого еквівалента beforeCreate немає - сам setup() запускається в той самий момент. Натомість отримуємо tree-shakable composables без this і просте повторне використання логіки.
// Composition API - той самий порядок, інша структура
import { ref, onMounted, onBeforeUnmount } from 'vue';
export default {
setup() {
const count = ref(0);
onMounted(() => console.log('mounted, count:', count.value)); // 0
onBeforeUnmount(() => console.log('очищення'));
return { count };
}
};Обидва API дають однаковий порядок хуків. Різниця тільки в організації коду.
Коли використовувати який хук
- Початкове завантаження даних:
created(Options) абоonMounted(Composition).createdпочинає запит до рендеру;onMountedбезпечніший для SSR і підходить для більшості випадків - Робота з DOM: тільки
mounted/onMounted. До цього моменту DOM просто не існує - Очищення при відході:
beforeUnmount/onBeforeUnmount. Тут скасовуй fetch-запити, очищай інтервали, знімай слухачі подій - Реакція на зміни:
updated/onUpdatedабо кращеwatchEffect. Не навантажуйupdatedважкою логікою - він спрацьовує при кожній реактивній зміні - SSR-сумісна логіка:
created/setupпрацюють і на сервері, і на клієнті;mountedтільки в браузері
Як це працює зсередини
Під час created/setup Vue сканує шаблон і обгортає властивості data/ref у Proxy-об'єкти для реактивності. При монтуванні Vue будує дерево віртуальних вузлів (vnode) і патчить їх у реальний DOM через createElementNS та insertBefore.
Зміна реактивного стану ставить перерендер у чергу через queueJob - кілька синхронних змін дають один DOM-патч. Тому updated спрацьовує один раз, навіть якщо ти змінив три властивості підряд. При знищенні компонента Vue викликає unmountComponent, прибирає vnode-вузли і зупиняє всі effect-watcher-и.
Типові помилки
Звернення до DOM у created
// Неправильно
created() {
this.$refs.box.style.color = 'red'; // TypeError: cannot read properties of undefined
}
// Правильно: перенести в mounted()
mounted() {
this.$refs.box.style.color = 'red'; // працює
}Без очищення в beforeUnmount
// Неправильно - інтервал продовжує працювати після знищення компонента
mounted() {
this.timer = setInterval(() => fetchUpdates(), 5000);
}
// Правильно
beforeUnmount() {
clearInterval(this.timer);
}API-запит у beforeMount
// Неправильно - блокує рендер, ламає SSR hydration
async beforeMount() {
this.user = await fetchUser(); // Vue 3 попереджає про async перед монтуванням
}
// Правильно: async setup() або onMounted
async setup() {
const user = await fetchUser();
return { user };
}Застарілий $refs в updated
// Неправильно - дочірній компонент міг ще не оновитися
updated() {
console.log(this.$refs.child.innerHTML); // можливо застарілий стан
}
// Правильно: чекати повного оновлення
updated() {
this.$nextTick(() => console.log(this.$refs.child.innerHTML));
}Порядок хуків батька і дочірнього компонента
Це місце, де багато розробників дивуються. Коли батько оновлює проп, порядок такий:
beforeUpdateбатькаbeforeUpdateдочірньогоupdatedдочірньогоupdatedбатька
Батько завершує оновлення останнім. Якщо треба прочитати стан дочірнього компонента з батьківського хука - роби це в updated, а не в beforeUpdate. У динамічних списках із вкладеними компонентами ця помилка дає непомітні баги зі застарілими даними.
Де зустрічається в реальних проектах
- Nuxt.js:
asyncDataуcreatedдля SSR-завантаження до гідратації - Pinia:
beforeUnmountприбирає підписки на стор, щоб не множити обробники - Element Plus:
mountedініціалізує позиціонування тултіпів після появи DOM - Quasar:
onBeforeUnmountзупиняє слухачі media query в компонентах лейауту - Fetch з відміною: зберігай
AbortControllerуsetup, скасовуй вonBeforeUnmount
Питання на співбесіді
Q: Яка різниця між mounted і nextTick?
A: mounted спрацьовує після того як Vue патчить vnode-дерево в DOM, але до того як браузер малює кадр. nextTick чекає повного очищення черги оновлень, включно з дочірніми компонентами. Якщо треба прочитати DOM дочірнього компонента після зміни стану - використовуй nextTick всередині updated.
Q: Як SSR впливає на хуки життєвого циклу?
A: На сервері beforeMount і mounted не запускаються взагалі. Тільки beforeCreate, created і setup. Логіку, яка має працювати і на сервері, розміщуй у setup або created.
Q: Який еквівалент beforeCreate в Composition API?
A: Прямого еквівалента немає. setup() запускається в той самий момент. Код на початку setup, до перших onX-викликів, і є цим еквівалентом.
Q: Коли спрацьовує activated з KeepAlive?
A: Коли кешований компонент повертається у відображення. Перший раз після mounted, потім замість mounted при кожному повторному показі. Пару activated/deactivated зручно використовувати для оновлення даних на кешованих маршрутах.
Q: У Teleport-компоненті всередині Suspense - який порядок хуків від suspend до resolve?
A: Suspense переходить у pending-стан, запускається setup/created дочірнього, асинхронна залежність вирішується, спрацьовує beforeUpdate батька, потім mounted дочірнього (Teleport змінює місце в DOM, але не порядок хуків), потім updated батька.
Приклади
Базовий: порядок хуків у консолі
// Options API - відстежуємо кожну фазу
export default {
name: 'OrderDemo',
data() { return { message: 'hello' }; },
beforeCreate() { console.log('beforeCreate - this.message:', this.message); }, // undefined
created() { console.log('created - this.message:', this.message); }, // 'hello'
mounted() { console.log('mounted - DOM готовий'); },
beforeUpdate() { console.log('beforeUpdate - перед ре-рендером'); },
updated() { console.log('updated - DOM оновлений'); },
beforeUnmount(){ console.log('beforeUnmount - останній шанс на очищення'); },
unmounted() { console.log('unmounted'); }
};Зміни message ззовні - побачиш beforeUpdate і updated один раз на зміну, незалежно від кількості присвоєнь всередині. Vue батчить оновлення.
Середній: картка профілю з API-запитом і очищенням
<template>
<div v-if="loading">Завантаження...</div>
<div v-else>{{ user.name }}</div>
</template>
<script>
export default {
data() {
return { user: null, loading: true, controller: null };
},
async mounted() {
this.controller = new AbortController();
try {
const res = await fetch('/api/profile', { signal: this.controller.signal });
this.user = await res.json();
} catch (e) {
if (e.name !== 'AbortError') console.error(e);
} finally {
this.loading = false;
}
},
beforeUnmount() {
// Користувач пішов зі сторінки до завершення запиту - скасовуємо
this.controller?.abort();
}
};
</script>Патерн з AbortController рятує від попереджень "can't set state on unmounted component" у SPA з швидкою навігацією між маршрутами.
Просунутий: порядок оновлення батько-дочірній
// Дочірній компонент логує свої хуки
const Child = {
props: ['items'],
beforeUpdate() { console.log('Child beforeUpdate'); },
updated() { console.log('Child updated'); },
template: '<ul><li v-for="i in items" :key="i">{{ i }}</li></ul>'
};
// Батько змінює проп через секунду
export default {
components: { Child },
data() { return { items: [] }; },
mounted() {
setTimeout(() => { this.items = [1, 2, 3]; }, 1000);
},
beforeUpdate() { console.log('Parent beforeUpdate'); },
updated() { console.log('Parent updated'); },
template: '<Child :items="items" />'
};
// Порядок у консолі:
// Parent beforeUpdate
// Child beforeUpdate
// Child updated
// Parent updatedupdated батька спрацьовує останнім. Якщо треба прочитати оновлений DOM дочірнього з батьківського хука - роби це в updated батька, не в beforeUpdate. Читання раніше дасть стан до оновлення.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.