Skip to main content

Компоненти 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

Швидкий приклад

js
// 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 → 6

data() ініціалізується між beforeCreate і created. DOM доступний тільки починаючи з mounted.

Options API проти Composition API

Хуки Options API (beforeCreate, mounted) оголошуються прямо в об'єкті компонента, як методи класу. Просто і зрозуміло, але логіка розкидана по різних секціях одного файлу.

Composition API загортає ту саму логіку в setup() через функції onMounted, onBeforeUnmount та інші. Прямого еквівалента beforeCreate немає - сам setup() запускається в той самий момент. Натомість отримуємо tree-shakable composables без this і просте повторне використання логіки.

js
// 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

js
// Неправильно created() { this.$refs.box.style.color = 'red'; // TypeError: cannot read properties of undefined } // Правильно: перенести в mounted() mounted() { this.$refs.box.style.color = 'red'; // працює }

Без очищення в beforeUnmount

js
// Неправильно - інтервал продовжує працювати після знищення компонента mounted() { this.timer = setInterval(() => fetchUpdates(), 5000); } // Правильно beforeUnmount() { clearInterval(this.timer); }

API-запит у beforeMount

js
// Неправильно - блокує рендер, ламає SSR hydration async beforeMount() { this.user = await fetchUser(); // Vue 3 попереджає про async перед монтуванням } // Правильно: async setup() або onMounted async setup() { const user = await fetchUser(); return { user }; }

Застарілий $refs в updated

js
// Неправильно - дочірній компонент міг ще не оновитися updated() { console.log(this.$refs.child.innerHTML); // можливо застарілий стан } // Правильно: чекати повного оновлення updated() { this.$nextTick(() => console.log(this.$refs.child.innerHTML)); }

Порядок хуків батька і дочірнього компонента

Це місце, де багато розробників дивуються. Коли батько оновлює проп, порядок такий:

  1. beforeUpdate батька
  2. beforeUpdate дочірнього
  3. updated дочірнього
  4. 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 батька.

Приклади

Базовий: порядок хуків у консолі

js
// 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-запитом і очищенням

vue
<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 з швидкою навігацією між маршрутами.

Просунутий: порядок оновлення батько-дочірній

js
// Дочірній компонент логує свої хуки 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 updated

updated батька спрацьовує останнім. Якщо треба прочитати оновлений DOM дочірнього з батьківського хука - роби це в updated батька, не в beforeUpdate. Читання раніше дасть стан до оновлення.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?