直接跳到內容

Transition

Vue 提供了兩個內置組件,可以幫助你製作基於狀態變化的過渡和動畫:

  • <Transition> 會在一個元素或組件進入和離開 DOM 時應用動畫。本章節會介紹如何使用它。

  • <TransitionGroup> 會在一個 v-for 列表中的元素或組件被插入,移動,或移除時應用動畫。我們將在下一章節中介紹。

除了這兩個組件,我們也可以通過其他技術手段來應用動畫,比如切換 CSS class 或用狀態綁定樣式來驅動動畫。這些其他的方法會在動畫技巧章節中展開。

<Transition> 組件

<Transition> 是一個內置組件,這意味著它在任意別的組件中都可以被使用,無需註冊。它可以將進入和離開動畫應用到通過默認插槽傳遞給它的元素或組件上。進入或離開可以由以下的條件之一觸發:

  • v-if 所觸發的切換
  • v-show 所觸發的切換
  • 由特殊元素 <component> 切換的動態組件
  • 改變特殊的 key 屬性

以下是最基本用法的示例:

template
<button @click="show = !show">Toggle</button>
<Transition>
  <p v-if="show">hello</p>
</Transition>
css
/* 下面我們會解釋這些 class 的用處 */
.v-enter-active,
.v-leave-active {
  transition: opacity 0.5s ease;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
}

hello

TIP

<Transition> 僅支持單個元素或組件作為其插槽內容。如果內容是一個組件,這個組件必須僅有一個根元素。

當一個 <Transition> 組件中的元素被插入或移除時,會發生下面這些事情:

  1. Vue 會自動檢測目標元素是否應用了 CSS 過渡或動畫。如果是,則一些 CSS 過渡 class 會在適當的時機被添加和移除。

  2. 如果有作為監聽器的 JavaScript 鉤子,這些鉤子函數會在適當時機被調用。

  3. 如果沒有探測到 CSS 過渡或動畫、也沒有提供 JavaScript 鉤子,那麼 DOM 的插入、刪除操作將在瀏覽器的下一個動畫幀後執行。

基於 CSS 的過渡效果

CSS 過渡 class

一共有 6 個應用於進入與離開過渡效果的 CSS class。

過渡圖示

  1. v-enter-from:進入動畫的起始狀態。在元素插入之前添加,在元素插入完成後的下一幀移除。

  2. v-enter-active:進入動畫的生效狀態。應用於整個進入動畫階段。在元素被插入之前添加,在過渡或動畫完成之後移除。這個 class 可以被用來定義進入動畫的持續時間、延遲與速度曲線類型。

  3. v-enter-to:進入動畫的結束狀態。在元素插入完成後的下一幀被添加 (也就是 v-enter-from 被移除的同時),在過渡或動畫完成之後移除。

  4. v-leave-from:離開動畫的起始狀態。在離開過渡效果被觸發時立即添加,在一幀後被移除。

  5. v-leave-active:離開動畫的生效狀態。應用於整個離開動畫階段。在離開過渡效果被觸發時立即添加,在過渡或動畫完成之後移除。這個 class 可以被用來定義離開動畫的持續時間、延遲與速度曲線類型。

  6. v-leave-to:離開動畫的結束狀態。在一個離開動畫被觸發後的下一幀被添加 (也就是 v-leave-from 被移除的同時),在過渡或動畫完成之後移除。

v-enter-activev-leave-active 給我們提供了為進入和離開動畫指定不同速度曲線的能力,我們將在下面的小節中看到一個示例。

為過渡效果命名

我們可以給 <Transition> 組件傳一個 name prop 來聲明一個過渡效果名:

template
<Transition name="fade">
  ...
</Transition>

對於一個有名字的過渡效果,對它起作用的過渡 class 會以其名字而不是 v 作為前綴。比如,上方例子中被應用的 class 將會是 fade-enter-active 而不是 v-enter-active。這個“fade”過渡的 class 應該是這樣:

css
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

CSS 的 transition

<Transition> 一般都會搭配原生 CSS 過渡一起使用,正如你在上面的例子中所看到的那樣。這個 transition CSS 屬性是一個簡寫形式,使我們可以一次定義一個過渡的各個方面,包括需要執行動畫的屬性、持續時間和速度曲線

下面是一個更高級的例子,它使用了不同的持續時間和速度曲線來過渡多個屬性:

template
<Transition name="slide-fade">
  <p v-if="show">hello</p>
</Transition>
css
/*
  進入和離開動畫可以使用不同
  持續時間和速度曲線。
*/
.slide-fade-enter-active {
  transition: all 0.3s ease-out;
}

.slide-fade-leave-active {
  transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);
}

.slide-fade-enter-from,
.slide-fade-leave-to {
  transform: translateX(20px);
  opacity: 0;
}

hello

CSS 的 animation

原生 CSS 動畫和 CSS transition 的應用方式基本上是相同的,只有一點不同,那就是 *-enter-from 不是在元素插入後立即移除,而是在一個 animationend 事件觸發時被移除。

對於大多數的 CSS 動畫,我們可以簡單地在 *-enter-active*-leave-active class 下聲明它們。下面是一個示例:

template
<Transition name="bounce">
  <p v-if="show" style="text-align: center;">
    Hello here is some bouncy text!
  </p>
</Transition>
css
.bounce-enter-active {
  animation: bounce-in 0.5s;
}
.bounce-leave-active {
  animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.25);
  }
  100% {
    transform: scale(1);
  }
}

Hello here is some bouncy text!

自定義過渡 class

你也可以向 <Transition> 傳遞以下的 props 來指定自定義的過渡 class:

  • enter-from-class
  • enter-active-class
  • enter-to-class
  • leave-from-class
  • leave-active-class
  • leave-to-class

你傳入的這些 class 會覆蓋相應階段的默認 class 名。這個功能在你想要在 Vue 的動畫機制下集成其他的第三方 CSS 動畫庫時非常有用,比如 Animate.css

template
<!-- 假設你已經在頁面中引入了 Animate.css -->
<Transition
  name="custom-classes"
  enter-active-class="animate__animated animate__tada"
  leave-active-class="animate__animated animate__bounceOutRight"
>
  <p v-if="show">hello</p>
</Transition>

同時使用 transition 和 animation

Vue 需要附加事件監聽器,以便知道過渡何時結束。可以是 transitionendanimationend,這取決於你所應用的 CSS 規則。如果你僅僅使用二者的其中之一,Vue 可以自動探測到正確的類型。

然而在某些場景中,你或許想要在同一個元素上同時使用它們兩個。舉例來說,Vue 觸發了一個 CSS 動畫,同時鼠標懸停觸發另一個 CSS 過渡。此時你需要顯式地傳入 type prop 來聲明,告訴 Vue 需要關心哪種類型,傳入的值是 animationtransition

template
<Transition type="animation">...</Transition>

深層級過渡與顯式過渡時長

儘管過渡 class 僅能應用在 <Transition> 的直接子元素上,我們還是可以使用深層級的 CSS 選擇器,在深層級的元素上觸發過渡效果:

template
<Transition name="nested">
  <div v-if="show" class="outer">
    <div class="inner">
      Hello
    </div>
  </div>
</Transition>
css
/* 應用於嵌套元素的規則 */
.nested-enter-active .inner,
.nested-leave-active .inner {
  transition: all 0.3s ease-in-out;
}

.nested-enter-from .inner,
.nested-leave-to .inner {
  transform: translateX(30px);
  opacity: 0;
}

/* ... 省略了其他必要的 CSS */

我們甚至可以在深層元素上添加一個過渡延遲,從而創建一個帶漸進延遲的動畫序列:

css
/* 延遲嵌套元素的進入以獲得交錯效果 */
.nested-enter-active .inner {
  transition-delay: 0.25s;
}

然而,這會帶來一個小問題。默認情況下,<Transition> 組件會通過監聽過渡根元素上的第一個 transitionend 或者 animationend 事件來嘗試自動判斷過渡何時結束。而在嵌套的過渡中,期望的行為應該是等待所有內部元素的過渡完成。

在這種情況下,你可以通過向 <Transition> 組件傳入 duration prop 來顯式指定過渡的持續時間 (以毫秒為單位)。總持續時間應該匹配延遲加上內部元素的過渡持續時間:

template
<Transition :duration="550">...</Transition>
Hello

在演練場中嘗試一下

如果有必要的話,你也可以用對象的形式傳入,分開指定進入和離開所需的時間:

template
<Transition :duration="{ enter: 500, leave: 800 }">...</Transition>

性能考量

你可能注意到我們上面例子中展示的動畫所用到的 CSS 屬性大多是 transformopacity 之類的。用這些屬性製作動畫非常高效,因為:

  1. 他們在動畫過程中不會影響到 DOM 結構,因此不會每一幀都觸發昂貴的 CSS 佈局重新計算。

  2. 大多數的現代瀏覽器都可以在執行 transform 動畫時利用 GPU 進行硬件加速。

相比之下,像 height 或者 margin 這樣的屬性會觸發 CSS 佈局變動,因此執行它們的動畫效果更昂貴,需要謹慎使用。我們可以在 CSS-Triggers 這類的網站查詢哪些屬性會在執行動畫時觸發 CSS 佈局變動。

JavaScript 鉤子

你可以通過監聽 <Transition> 組件事件的方式在過渡過程中掛上鉤子函數:

html
<Transition
  @before-enter="onBeforeEnter"
  @enter="onEnter"
  @after-enter="onAfterEnter"
  @enter-cancelled="onEnterCancelled"
  @before-leave="onBeforeLeave"
  @leave="onLeave"
  @after-leave="onAfterLeave"
  @leave-cancelled="onLeaveCancelled"
>
  <!-- ... -->
</Transition>
js
// 在元素被插入到 DOM 之前被調用
// 用這個來設置元素的 "enter-from" 狀態
function onBeforeEnter(el) {}

// 在元素被插入到 DOM 之後的下一幀被調用
// 用這個來開始進入動畫
function onEnter(el, done) {
  // 調用回調函數 done 表示過渡結束
  // 如果與 CSS 結合使用,則這個回調是可選參數
  done()
}

// 當進入過渡完成時調用。
function onAfterEnter(el) {}

// 當進入過渡在完成之前被取消時調用
function onEnterCancelled(el) {}

// 在 leave 鉤子之前調用
// 大多數時候,你應該只會用到 leave 鉤子
function onBeforeLeave(el) {}

// 在離開過渡開始時調用
// 用這個來開始離開動畫
function onLeave(el, done) {
  // 調用回調函數 done 表示過渡結束
  // 如果與 CSS 結合使用,則這個回調是可選參數
  done()
}

// 在離開過渡完成、
// 且元素已從 DOM 中移除時調用
function onAfterLeave(el) {}

// 僅在 v-show 過渡中可用
function onLeaveCancelled(el) {}
js
export default {
  // ...
  methods: {
    // 在元素被插入到 DOM 之前被調用
    // 用這個來設置元素的 "enter-from" 狀態
    onBeforeEnter(el) {},

    // 在元素被插入到 DOM 之後的下一幀被調用
    // 用這個來開始進入動畫
    onEnter(el, done) {
      // 調用回調函數 done 表示過渡結束
      // 如果與 CSS 結合使用,則這個回調是可選參數
      done()
    },

    // 當進入過渡完成時調用。
    onAfterEnter(el) {},

    // 當進入過渡在完成之前被取消時調用
    onEnterCancelled(el) {},

    // 在 leave 鉤子之前調用
    // 大多數時候,你應該只會用到 leave 鉤子
    onBeforeLeave(el) {},

    // 在離開過渡開始時調用
    // 用這個來開始離開動畫
    onLeave(el, done) {
      // 調用回調函數 done 表示過渡結束
      // 如果與 CSS 結合使用,則這個回調是可選參數
      done()
    },

    // 在離開過渡完成、
    // 且元素已從 DOM 中移除時調用
    onAfterLeave(el) {},

    // 僅在 v-show 過渡中可用
    onLeaveCancelled(el) {}
  }
}

這些鉤子可以與 CSS 過渡或動畫結合使用,也可以單獨使用。

在使用僅由 JavaScript 執行的動畫時,最好是添加一個 :css="false" prop。這顯式地向 Vue 表明可以跳過對 CSS 過渡的自動探測。除了性能稍好一些之外,還可以防止 CSS 規則意外地干擾過渡效果:

template
<Transition
  ...
  :css="false"
>
  ...
</Transition>

在有了 :css="false" 後,我們就自己全權負責控制什麼時候過渡結束了。這種情況下對於 @enter@leave 鉤子來說,回調函數 done 就是必須的。否則,鉤子將被同步調用,過渡將立即完成。

這裡是使用 GSAP 庫執行動畫的一個示例,你也可以使用任何你想要的庫,比如 Anime.js 或者 Motion One

可複用過渡效果

得益於 Vue 的組件系統,過渡效果是可以被封裝複用的。要創建一個可被複用的過渡,我們需要為 <Transition> 組件創建一個包裝組件,並向內傳入插槽內容:

vue
<!-- MyTransition.vue -->
<script>
// JavaScript 鉤子邏輯...
</script>

<template>
  <!-- 包裝內置的 Transition 組件 -->
  <Transition
    name="my-transition"
    @enter="onEnter"
    @leave="onLeave">
    <slot></slot> <!-- 向內傳遞插槽內容 -->
  </Transition>
</template>

<style>
/*
  必要的 CSS...
  注意:避免在這裡使用 <style scoped>
  因為那不會應用到插槽內容上
*/
</style>

現在 MyTransition 可以在導入後像內置組件那樣使用了:

template
<MyTransition>
  <div v-if="show">Hello</div>
</MyTransition>

出現時過渡

如果你想在某個節點初次渲染時應用一個過渡效果,你可以添加 appear prop:

template
<Transition appear>
  ...
</Transition>

元素間過渡

除了通過 v-if / v-show 切換一個元素,我們也可以通過 v-if / v-else / v-else-if 在幾個組件間進行切換,只要確保任一時刻只會有一個元素被渲染即可:

template
<Transition>
  <button v-if="docState === 'saved'">Edit</button>
  <button v-else-if="docState === 'edited'">Save</button>
  <button v-else-if="docState === 'editing'">Cancel</button>
</Transition>
Click to cycle through states:

在演練場中嘗試一下

過渡模式

在之前的例子中,進入和離開的元素都是在同時開始動畫的,因此我們不得不將它們設為 position: absolute 以避免二者同時存在時出現的佈局問題。

然而,很多情況下這可能並不符合需求。我們可能想要先執行離開動畫,然後在其完成之後再執行元素的進入動畫。手動編排這樣的動畫是非常複雜的,好在我們可以通過向 <Transition> 傳入一個 mode prop 來實現這個行為:

template
<Transition mode="out-in">
  ...
</Transition>

將之前的例子改為 mode="out-in" 後是這樣:

Click to cycle through states:

<Transition> 也支持 mode="in-out",雖然這並不常用。

組件間過渡

<Transition> 也可以作用於動態組件之間的切換:

template
<Transition name="fade" mode="out-in">
  <component :is="activeComponent"></component>
</Transition>
Component A

動態過渡

<Transition> 的 props (比如 name) 也可以是動態的!這讓我們可以根據狀態變化動態地應用不同類型的過渡:

template
<Transition :name="transitionName">
  <!-- ... -->
</Transition>

這個特性的用處是可以提前定義好多組 CSS 過渡或動畫的 class,然後在它們之間動態切換。

你也可以根據你的組件的當前狀態在 JavaScript 過渡鉤子中應用不同的行為。最後,創建動態過渡的終極方式還是創建可複用的過渡組件,並讓這些組件根據動態的 props 來改變過渡的效果。掌握了這些技巧後,就真的只有你想不到,沒有做不到的了。

使用 Key 屬性過渡

有時為了觸發過渡,你需要強制重新渲染 DOM 元素。

以計數器組件為例:

vue
<script setup>
import { ref } from 'vue';
const count = ref(0);

setInterval(() => count.value++, 1000);
</script>

<template>
  <Transition>
    <span :key="count">{{ count }}</span>
  </Transition>
</template>
vue
<script>
export default {
  data() {
    return {
      count: 1,
      interval: null 
    }
  },
  mounted() {
    this.interval = setInterval(() => {
      this.count++;
    }, 1000)
  },
  beforeDestroy() {
    clearInterval(this.interval)
  }
}
</script>

<template>
  <Transition>
    <span :key="count">{{ count }}</span>
  </Transition>
</template>

如果不使用 key 屬性,則只有文本節點會被更新,因此不會發生過渡。但是,有了 key 屬性,Vue 就知道在 count 改變時創建一個新的 span 元素,因此 Transition 組件有兩個不同的元素在它們之間進行過渡。


參考

Transition已經加載完畢