Vue3 轉場與動畫 – JavaScript Hook 動畫深入剖析
簡介
在單頁應用(SPA)裡,畫面的切換與元件的顯示/隱藏往往會伴隨著動畫效果。適當的動畫不只提升使用者體驗,還能協助使用者理解介面的變化脈絡。Vue 3 內建的 <transition>、<transition-group> 組件已經支援 CSS 動畫 與 CSS 轉場,但在實務開發中,我們常會遇到需要根據資料狀態或外部條件動態控制動畫的情境,這時 JavaScript hook(即 Vue 提供的過渡生命週期函式)就顯得格外重要。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶你掌握在 Vue 3 中使用 JavaScript hook 來打造彈性、可控且效能佳的動畫效果。文章採用繁體中文(台灣),適合剛入門 Vue 的新手以及想提升動畫技巧的中階開發者。
核心概念
1. 為什麼需要 JavaScript hook?
- 動態計算:有時動畫的起始或結束狀態必須根據實際 DOM 大小、位置或資料內容計算,如展開手風琴、彈出提示框等。
- 同步非 CSS 動畫:若要與第三方動畫函式庫(如 GSAP、Anime.js)或 Canvas、WebGL 效果同步,純 CSS 難以達成。
- 控制流程:可以在動畫開始前、結束後執行額外的業務邏輯(如載入資料、切換路由、觸發事件)。
Vue 3 的 <transition> 提供了 enter、leave、appear 三大階段的 JavaScript hook,分別對應 進入、離開、首次渲染 時的動畫時機。每個階段都有 before、***(實際執行)**、after 三個子鉤子,總共有 9 個可供使用的函式:
| Hook 名稱 | 說明 |
|---|---|
onBeforeEnter |
元素插入前,尚未掛載到 DOM 前的最後一次機會。 |
onEnter |
元素已掛載到 DOM,可開始動畫。須手動呼叫 done 以結束。 |
onAfterEnter |
動畫結束後呼叫,常用於清理或觸發後續流程。 |
onEnterCancelled |
進入動畫被取消時呼叫。 |
onBeforeLeave |
離開前的最後一次機會。 |
onLeave |
開始離開動畫,同樣需要 done。 |
onAfterLeave |
離開動畫結束後。 |
onLeaveCancelled |
離開動畫被取消時。 |
onBeforeAppear / onAppear / onAfterAppear |
首次渲染時的動畫(在 appear 屬性開啟時)。 |
小技巧:若在
onEnter/onLeave中直接使用setTimeout或requestAnimationFrame,務必要在最後呼叫done(),否則 Vue 會認為動畫仍在進行,導致後續渲染被阻塞。
2. 基本使用方式
在 Vue 3 中,我們通常以 Composition API 搭配 <script setup> 撰寫,以下是一個最簡單的範例:
<template>
<button @click="show = !show">Toggle</button>
<transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
>
<div v-if="show" class="box">Hello Vue 3</div>
</transition>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(false)
// 進入動畫:先把元素透明度設為 0,然後淡入
function onBeforeEnter(el) {
el.style.opacity = 0
}
function onEnter(el, done) {
// 使用 requestAnimationFrame 讓瀏覽器先渲染初始狀態
requestAnimationFrame(() => {
el.style.transition = 'opacity 300ms ease'
el.style.opacity = 1
// 監聽 transitionend 事件,完成後呼叫 done
el.addEventListener('transitionend', done, { once: true })
})
}
function onAfterEnter(el) {
el.style.transition = '' // 清除 inline style,避免干擾後續動畫
}
// 離開動畫:淡出
function onBeforeLeave(el) {
el.style.opacity = 1
}
function onLeave(el, done) {
requestAnimationFrame(() => {
el.style.transition = 'opacity 300ms ease'
el.style.opacity = 0
el.addEventListener('transitionend', done, { once: true })
})
}
function onAfterLeave(el) {
el.style.transition = ''
}
</script>
<style scoped>
.box {
width: 200px;
height: 100px;
background: #42b983;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
</style>
這段程式碼示範了 enter 與 leave 的完整流程,且使用
requestAnimationFrame確保過渡屬性在下一個重繪週期才套用,避免「跳躍」的情況。
3. 搭配第三方動畫函式庫(GSAP)
如果動畫需求超過 CSS 能力,像是彈性曲線、序列動畫、物理彈跳等,GSAP(GreenSock Animation Platform)是常見的選擇。以下示範在 onEnter、onLeave 中使用 GSAP:
<template>
<button @click="open = !open">Toggle Card</button>
<transition
@enter="enterCard"
@leave="leaveCard"
mode="out-in"
>
<div v-if="open" class="card">GSAP 動畫卡片</div>
</transition>
</template>
<script setup>
import { ref } from 'vue'
import { gsap } from 'gsap'
const open = ref(false)
function enterCard(el, done) {
// 初始狀態:縮小、透明
gsap.set(el, { scale: 0.5, opacity: 0 })
// 播放動畫
gsap.to(el, {
duration: 0.6,
scale: 1,
opacity: 1,
ease: 'back.out(1.7)',
onComplete: done // 告訴 Vue 動畫已完成
})
}
function leaveCard(el, done) {
gsap.to(el, {
duration: 0.4,
scale: 0.3,
opacity: 0,
ease: 'power1.in',
onComplete: done
})
}
</script>
<style scoped>
.card {
width: 250px;
height: 150px;
background: #ff6f61;
color: #fff;
line-height: 150px;
text-align: center;
border-radius: 8px;
}
</style>
重點:使用外部函式庫時,仍必須在動畫結束時呼叫
done(或返回一個Promise),否則 Vue 會一直等待,導致過渡卡住。
4. 取得實際尺寸與位置 – 動態 Height Transition
常見的需求是 高度自動伸縮(如手風琴、折疊面板),純 CSS height: auto 無法平滑過渡。透過 JavaScript hook,我們可以在進入時測量內容高度,並以像素值做過渡:
<template>
<button @click="expanded = !expanded">Toggle Accordion</button>
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
>
<div v-show="expanded" class="panel">
<p>這是一段可變長度的文字,會依內容自動調整高度。</p>
<p>更多內容...</p>
</div>
</transition>
</template>
<script setup>
import { ref } from 'vue'
const expanded = ref(false)
// 進入前先把高度設為 0,隱藏 overflow
function beforeEnter(el) {
el.style.height = '0'
el.style.overflow = 'hidden'
}
// 取得實際高度後做過渡
function enter(el, done) {
const height = el.scrollHeight
el.style.transition = 'height 300ms ease'
requestAnimationFrame(() => {
el.style.height = height + 'px'
})
el.addEventListener('transitionend', done, { once: true })
}
// 完成後清理樣式
function afterEnter(el) {
el.style.height = ''
el.style.overflow = ''
}
// 離開前設定當前高度,讓過渡從此高度開始
function beforeLeave(el) {
el.style.height = el.scrollHeight + 'px'
el.style.overflow = 'hidden'
}
// 離開時縮回 0
function leave(el, done) {
el.style.transition = 'height 300ms ease'
requestAnimationFrame(() => {
el.style.height = '0'
})
el.addEventListener('transitionend', done, { once: true })
}
// 清理
function afterLeave(el) {
el.style.height = ''
el.style.overflow = ''
}
</script>
<style scoped>
.panel {
background: #f0f4f8;
padding: 1rem;
border: 1px solid #cbd5e0;
}
</style>
這個範例展示了 測量真實高度、設定過渡、清除樣式 的完整流程,是實務中最常見的需求之一。
5. 多元素同時動畫 – Transition Group 與 JavaScript Hook
<transition-group> 讓列表項目在新增、刪除、重新排序時都能動畫。若要在每個項目上使用 JavaScript hook,只需要把 hook 綁定在 <transition-group> 上,並在 v-for 的子元素上加上 key:
<template>
<button @click="addItem">Add</button>
<button @click="removeItem">Remove</button>
<transition-group
name="list"
tag="ul"
@enter="enterItem"
@leave="leaveItem"
>
<li v-for="item in items" :key="item.id" class="list-item">
{{ item.text }}
</li>
</transition-group>
</template>
<script setup>
import { ref } from 'vue'
let id = 0
const items = ref([])
function addItem() {
items.value.push({ id: ++id, text: `項目 ${id}` })
}
function removeItem() {
items.value.pop()
}
// 單一項目的進入動畫
function enterItem(el, done) {
el.style.opacity = 0
el.style.transform = 'translateY(-20px)'
requestAnimationFrame(() => {
el.style.transition = 'all 300ms ease'
el.style.opacity = 1
el.style.transform = 'translateY(0)'
el.addEventListener('transitionend', done, { once: true })
})
}
// 離開動畫
function leaveItem(el, done) {
el.style.transition = 'all 300ms ease'
el.style.opacity = 0
el.style.transform = 'translateY(20px)'
el.addEventListener('transitionend', done, { once: true })
}
</script>
<style scoped>
ul {
list-style: none;
padding: 0;
}
.list-item {
background: #e2e8f0;
margin: 4px 0;
padding: 8px 12px;
border-radius: 4px;
}
</style>
注意:
<transition-group>內的子元素 必須有唯一的key,否則 Vue 無法正確追蹤每筆資料的進出,導致動畫異常。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方式 |
|---|---|---|
忘記呼叫 done() |
動畫永遠不會結束,導致後續渲染被阻塞,甚至卡住 UI。 | 在 transitionend、animationend 或動畫函式的回呼中 一定 呼叫 done(),或回傳 Promise。 |
在 enter / leave 中直接寫同步 CSS |
瀏覽器會把變更視為同一幀,導致過渡不會觸發。 | 使用 requestAnimationFrame 或 nextTick() 把樣式改變推遲到下一個渲染週期。 |
使用 v-show 而非 v-if 時忘記加 appear |
首次渲染不會觸發 appear 鉤子,動畫失效。 |
若需要首次顯示動畫,將 <transition appear> 加上 appear 屬性,或改用 v-if。 |
在 enter / leave 中直接改變 display |
會破壞過渡流程,導致 transitionend 無法觸發。 |
只改變 opacity、transform、height 等可過渡屬性,display 交給 Vue 控制。 |
過度使用 setTimeout |
產生不一致的動畫時間,且難以維護。 | 盡量使用 CSS transition / animation 的原生事件,或第三方函式庫的回呼。 |
| 忘記在離開動畫結束後清除 inline style | 下一次進入時會受到前一次的樣式遺留影響。 | 在 afterEnter / afterLeave 中 重設 相關樣式 (el.style = '')。 |
最佳實踐
- 保持 Hook 簡潔:只負責動畫本身,業務邏輯應放在
afterEnter/afterLeave或外部的watch中。 - 使用
once: true監聽transitionend/animationend,避免重複觸發done()。 - 對於需要測量尺寸的動畫,使用
el.scrollHeight、el.getBoundingClientRect(),並 在requestAnimationFrame之後設定過渡。 - 避免同時在 CSS 與 JavaScript 中設定相同屬性,容易產生衝突。若需要混合,優先在 JavaScript 中設定,結束後清除。
- 在大型專案中,可抽離共用的動畫 Hook 為 Composable(如
useFadeTransition()),提升可重用性與維護性。
實際應用場景
| 場景 | 為何適合使用 JavaScript hook | 範例概念 |
|---|---|---|
| 彈出式對話框 | 需要在顯示前先測量視窗尺寸、在關閉後回收資源(如移除事件監聽) | 進入時使用 scale + opacity,離開時回到 scale 0.8,並在 afterLeave 移除全域鍵盤監聽。 |
| 手風琴 / 折疊面板 | 高度必須根據內容自動伸縮,且可能同時有多個面板同時操作 | 參考「動態 Height Transition」範例,搭配 v-for 產生多筆資料。 |
| 列表排序動畫 | 列表項目重新排列時,需要同時維持進場與離場動畫 | 使用 <transition-group> 並在 enter / leave 中加入位移與淡入淡出。 |
| 載入資料的過渡 | 在資料請求期間顯示 loading 動畫,完成後淡出並顯示內容 | 在 onEnter 開始時觸發 API,onAfterEnter 隱藏 loading。 |
| 與第三方繪圖庫結合 | 如在 Canvas 上做粒子特效,需要同步 Vue 元件的顯示/隱藏 | 在 onEnter 中初始化粒子,onLeave 中銷毀實例,確保資源不泄漏。 |
總結
Vue 3 的 JavaScript hook 為動畫提供了前所未有的彈性與控制力。透過 onEnter、onLeave 等鉤子,我們可以:
- 動態計算 元素尺寸或位置,完成高度自動伸縮等需求。
- 結合第三方函式庫(如 GSAP、Anime.js)打造更複雜的動畫效果。
- 同步業務流程,在動畫完成後才執行資料載入、路由切換等操作。
在實務開發時,務必記得 呼叫 done()、使用 requestAnimationFrame 以避免 CSS 與 JavaScript 的衝突,並在動畫結束後 清除 inline style,保持元件的乾淨狀態。透過本文提供的多個範例與最佳實踐,你已掌握在 Vue 3 中運用 JavaScript hook 製作專業動畫的核心技巧,接下來可以把這些知識套用到自己的專案,為使用者帶來更流暢、直觀的互動體驗。祝開發順利!