本文 AI 產出,尚未審核

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> 提供了 enterleaveappear 三大階段的 JavaScript hook,分別對應 進入離開首次渲染 時的動畫時機。每個階段都有 before、***(實際執行)**、after 三個子鉤子,總共有 9 個可供使用的函式:

Hook 名稱 說明
onBeforeEnter 元素插入前,尚未掛載到 DOM 前的最後一次機會。
onEnter 元素已掛載到 DOM,可開始動畫。須手動呼叫 done 以結束。
onAfterEnter 動畫結束後呼叫,常用於清理或觸發後續流程。
onEnterCancelled 進入動畫被取消時呼叫。
onBeforeLeave 離開前的最後一次機會。
onLeave 開始離開動畫,同樣需要 done
onAfterLeave 離開動畫結束後。
onLeaveCancelled 離開動畫被取消時。
onBeforeAppear / onAppear / onAfterAppear 首次渲染時的動畫(在 appear 屬性開啟時)。

小技巧:若在 onEnter / onLeave 中直接使用 setTimeoutrequestAnimationFrame,務必要在最後呼叫 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>

這段程式碼示範了 enterleave 的完整流程,且使用 requestAnimationFrame 確保過渡屬性在下一個重繪週期才套用,避免「跳躍」的情況。


3. 搭配第三方動畫函式庫(GSAP)

如果動畫需求超過 CSS 能力,像是彈性曲線、序列動畫、物理彈跳等,GSAP(GreenSock Animation Platform)是常見的選擇。以下示範在 onEnteronLeave 中使用 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。 transitionendanimationend 或動畫函式的回呼中 一定 呼叫 done(),或回傳 Promise
enter / leave 中直接寫同步 CSS 瀏覽器會把變更視為同一幀,導致過渡不會觸發。 使用 requestAnimationFramenextTick() 把樣式改變推遲到下一個渲染週期。
使用 v-show 而非 v-if 時忘記加 appear 首次渲染不會觸發 appear 鉤子,動畫失效。 若需要首次顯示動畫,將 <transition appear> 加上 appear 屬性,或改用 v-if
enter / leave 中直接改變 display 會破壞過渡流程,導致 transitionend 無法觸發。 只改變 opacitytransformheight 等可過渡屬性,display 交給 Vue 控制。
過度使用 setTimeout 產生不一致的動畫時間,且難以維護。 盡量使用 CSS transition / animation 的原生事件,或第三方函式庫的回呼。
忘記在離開動畫結束後清除 inline style 下一次進入時會受到前一次的樣式遺留影響。 afterEnter / afterLeave重設 相關樣式 (el.style = '')。

最佳實踐

  1. 保持 Hook 簡潔:只負責動畫本身,業務邏輯應放在 afterEnter / afterLeave 或外部的 watch 中。
  2. 使用 once: true 監聽 transitionend / animationend,避免重複觸發 done()
  3. 對於需要測量尺寸的動畫,使用 el.scrollHeightel.getBoundingClientRect(),並 requestAnimationFrame 之後設定過渡。
  4. 避免同時在 CSS 與 JavaScript 中設定相同屬性,容易產生衝突。若需要混合,優先在 JavaScript 中設定,結束後清除。
  5. 在大型專案中,可抽離共用的動畫 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 為動畫提供了前所未有的彈性與控制力。透過 onEnteronLeave 等鉤子,我們可以:

  • 動態計算 元素尺寸或位置,完成高度自動伸縮等需求。
  • 結合第三方函式庫(如 GSAP、Anime.js)打造更複雜的動畫效果。
  • 同步業務流程,在動畫完成後才執行資料載入、路由切換等操作。

在實務開發時,務必記得 呼叫 done()使用 requestAnimationFrame 以避免 CSS 與 JavaScript 的衝突,並在動畫結束後 清除 inline style,保持元件的乾淨狀態。透過本文提供的多個範例與最佳實踐,你已掌握在 Vue 3 中運用 JavaScript hook 製作專業動畫的核心技巧,接下來可以把這些知識套用到自己的專案,為使用者帶來更流暢、直觀的互動體驗。祝開發順利!