Перехід та transitiongroup у Vue.js
Vue Transition - вбудований компонент, який обгортає один елемент і додає CSS-класи в потрібний момент, щоб анімації входу та виходу працювали без жодних зовнішніх бібліотек.
Теорія
TL;DR
<Transition>анімує один елемент;<TransitionGroup>анімує списокv-for- Vue автоматично додає шість CSS-класів:
v-enter-from,v-enter-active,v-enter-toпри вході та такі жleave-варіанти при виході - Встанови
name="fade"і префіксv-станеfade- mode="out-in"запобігає накладанню двох компонентів під час перемикання- JavaScript-хуки (
@enter,@leave) дозволяють підключити GSAP або будь-яку іншу бібліотеку, коли CSS недостатньо
Швидкий приклад
<template>
<button @click="show = !show">Перемкнути</button>
<Transition name="fade">
<p v-if="show">Привіт!</p>
</Transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0; /* приховано на початку входу і в кінці виходу */
}
</style>Vue додає .fade-enter-from і .fade-enter-active в той кадр, коли <p> потрапляє в DOM. Через один кадр додається .fade-enter-to і прибирається enter-from. Коли перехід завершується, всі класи видаляються. При виході все відбувається у зворотньому порядку.
Потік CSS-класів
Шість класів йдуть у фіксованій послідовності. Префікс за замовчуванням v-, або те, що передаєш у name:
ENTER: v-enter-from → v-enter-active → v-enter-to
LEAVE: v-leave-from → v-leave-active → v-leave-to| Клас | Коли застосовується |
|---|---|
fade-enter-from | Перший кадр входу (початковий стан) |
fade-enter-active | Весь час входу |
fade-enter-to | Останній кадр входу (кінцевий стан) |
fade-leave-from | Перший кадр виходу (початковий стан) |
fade-leave-active | Весь час виходу |
fade-leave-to | Останній кадр виходу (кінцевий стан) |
У клас -active ти пишеш transition або animation. У -from і -to описуєш початковий і кінцевий стани. Такий поділ дозволяє вільно комбінувати CSS-переходи та CSS-анімації.
TransitionGroup для списків
<TransitionGroup> додає те, чого <Transition> не вміє: анімацію переміщення (move transition), коли елементи списку змінюють порядок. Vue використовує техніку FLIP під капотом: фіксує позиції до і після зміни DOM, потім анімує різницю через transform.
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</TransitionGroup>
<style>
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 0.3s ease; /* анімація перерозташування */
}
.list-leave-active {
position: absolute; /* виводимо елемент з потоку */
}
</style>Клас .list-move тут головний. Без нього елементи просто стрибають на нові місця при видаленні сусіда. А position: absolute для .list-leave-active потрібен тому, що елемент, який виходить, ще займає місце в макеті під час анімації. Без цього решта елементів не зможе плавно заповнити звільнений простір.
Режими переходу
За замовчуванням <Transition> запускає вхід і вихід одночасно. При перемиканні компонентів це зазвичай призводить до накладання. mode="out-in" це виправляє.
<!-- поточний компонент повністю виходить, потім входить новий -->
<Transition name="fade" mode="out-in">
<component :is="currentView" :key="currentView" />
</Transition>Завжди додавай :key на компонент всередині <Transition>. Без нього Vue може повторно використати той самий DOM-вузол і анімація взагалі не спрацює.
mode="in-out" робить навпаки: новий компонент входить, поки старий ще видимий, потім старий виходить. Зустрічається рідше, але підходить для ефектів накладання шарів.
JavaScript-хуки
CSS вирішує більшість завдань. Коли потрібен таймлайн, фізика пружини або SVG-морфінг, використовуй хуки і передавай управління бібліотеці:
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@leave="onLeave"
:css="false"
>
<div v-if="show">Контент</div>
</Transition>
<script setup>
import gsap from 'gsap'
function onBeforeEnter(el) {
gsap.set(el, { opacity: 0, y: -20 })
}
function onEnter(el, done) {
gsap.to(el, { opacity: 1, y: 0, duration: 0.4, onComplete: done })
}
function onLeave(el, done) {
gsap.to(el, { opacity: 0, y: 20, duration: 0.3, onComplete: done })
}
</script>Встановлюй :css="false", коли JS-хуки виконують всю роботу. Це вимикає ін'єкцію класів у Vue. Якщо пропустити, Vue може викликати done зарано і конфліктувати з таймінгом бібліотеки.
Типові помилки
Відсутній :key на динамічних компонентах
<!-- Vue повторно використовує DOM-вузол - анімація не спрацьовує -->
<Transition name="fade" mode="out-in">
<component :is="currentView" />
</Transition>
<!-- правильно -->
<Transition name="fade" mode="out-in">
<component :is="currentView" :key="currentView" />
</Transition>Забули position: absolute для .list-leave-active
Без цього елемент, що виходить, зберігає місце в макеті під час анімації. Решта елементів не рухається, поки Vue його не видалить, тому анімація перерозташування виглядає зламаною. Це найпоширеніша проблема з <TransitionGroup>, яку я бачу на code review.
Не викликається done() у JS-хуках
Хуки @enter і @leave отримують колбек done. Якщо його не викликати, Vue вважає, що перехід все ще триває. Елемент залишається в стані переходу безкінечно, і жодні наступні анімації на ньому не спрацюють.
Кілька елементів всередині <Transition>
<Transition> очікує рівно один дочірній елемент. Два сусідніх елементи всередині викличуть попередження в runtime. Загорни їх в один div або переходь до <TransitionGroup>.
Де зустрічається
- Модальні вікна:
<Transition name="modal" mode="out-in">для оверлея - Вкладки:
mode="out-in"на<component :is="activeTab"> - Сповіщення-тости:
<TransitionGroup>зі слайдом зверху - Переходи між сторінками у Vue Router: обгортаємо
<RouterView>в<Transition> - Покрокові анімації списку: хук
@enterзel.dataset.indexдля затримки кожного елемента
Питання на співбесіді
Q: Яка різниця між <Transition> і <TransitionGroup>?
A: <Transition> анімує один елемент при вході або виході. <TransitionGroup> анімує список і додатково обробляє переміщення елементів при зміні порядку, використовуючи FLIP під капотом.
Q: Навіщо існує mode="out-in"?
A: Без нього вхід і вихід відбуваються одночасно. При перемиканні видів це спричиняє накладання двох панелей. out-in спочатку завершує вихід, потім починає вхід.
Q: Що станеться, якщо не викликати done() у JS-хуці?
A: Vue вважатиме перехід незавершеним. Елемент залишиться в стані переходу безкінечно, і жодні наступні анімації на ньому не запустяться.
Q: Як <TransitionGroup> анімує переміщення елементів?
A: Використовує FLIP: фіксує позиції до і після зміни DOM, застосовує інвертований transform, щоб елементи здавалися на старому місці, потім переходить до нульового значення. Клас .list-move керує тривалістю цього переходу.
Q: Коли використовувати :css="false"?
A: Коли JS-хуки роблять всю анімаційну роботу. Це не дає Vue додавати класи переходу, які можуть конфліктувати з таймінгом зовнішньої бібліотеки.
Приклади
Базове появлення та зникнення
<template>
<button @click="show = !show">Показати повідомлення</button>
<Transition name="fade">
<p v-if="show" class="notice">Збережено успішно.</p>
</Transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.25s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>Два CSS-правила, без JavaScript. Повідомлення з'являється при show = true і зникає при show = false.
Перемикання видів з mode="out-in"
<template>
<nav>
<button @click="view = 'HomeView'">Головна</button>
<button @click="view = 'ProfileView'">Профіль</button>
</nav>
<Transition name="slide" mode="out-in">
<component :is="view" :key="view" />
</Transition>
</template>
<style>
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease, opacity 0.3s ease;
}
.slide-enter-from {
transform: translateX(20px);
opacity: 0;
}
.slide-leave-to {
transform: translateX(-20px);
opacity: 0;
}
</style>mode="out-in" змушує поточний вид слайдитися вліво і зникати, перш ніж наступний з'явиться справа. Без mode обидва анімуються одночасно і перекриваються.
Список із сортуванням та анімацією переміщення
<template>
<button @click="shuffle">Перемішати</button>
<TransitionGroup name="cards" tag="ul" class="card-list">
<li v-for="card in cards" :key="card.id" class="card">
{{ card.label }}
</li>
</TransitionGroup>
</template>
<script setup>
import { ref } from 'vue'
const cards = ref([
{ id: 1, label: 'Картка A' },
{ id: 2, label: 'Картка B' },
{ id: 3, label: 'Картка C' },
])
function shuffle() {
cards.value = [...cards.value].sort(() => Math.random() - 0.5)
}
</script>
<style>
.card-list {
position: relative; /* потрібен для абсолютного позиціонування карток при виході */
list-style: none;
padding: 0;
}
.cards-move {
transition: transform 0.4s ease;
}
.cards-enter-active,
.cards-leave-active {
transition: all 0.3s ease;
}
.cards-enter-from,
.cards-leave-to {
opacity: 0;
transform: scale(0.8);
}
.cards-leave-active {
position: absolute;
}
</style>Кожне перемішування запускає FLIP для кожної картки, що змінила позицію. Клас .cards-move керує плавним переміщенням. position: relative на батьківському елементі потрібен, щоб картки, що виходять, залишались на місці візуально під час анімації виходу.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.