Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Компоненти Vue та їх життєві цикли». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Життєвий цикл компонента Vue (Vue component lifecycle)** - це фази від створення до видалення з хуками для виконання коду на кожній з них. ```js // Options API // Composition API beforeCreate() {} // початок setup() created() {} // початок setup() mounted() {} onMounted(() => {}) beforeUpdate() {} onBeforeUpdate(() => {}) updated() {} onUpdated(() => {}) beforeUnmount() {} onBeforeUnmount(() => {}) ``` **Головне правило:** DOM доступний тільки з `mounted`/`onMounted`. `beforeUnmount`/`onBeforeUnmount` - місце для скасування таймерів і fetch-запитів.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Життєвий цикл компонента 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`. Читання раніше дасть стан до оновлення.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.