請啟用 Javascript 查看內容

Vue筆記 - Transition

 ·  ☕ 12 分鐘

這週是 六角鼠年鐵人賽 第五週,紀錄一下 Vue Transition 用法。

前言

在日常開發中,網頁動態效果是必不可少的一部分,不僅能讓元素直接的切換顯得更加自然,同時也能極大的增強用戶體驗。

雖然我們可以使用原生 CSS 或 JS 來實現動態效果,但 Vue 提供了更簡單和高效的方式。

1. Vue 的轉場工具

Vue 在插入、更新或者移除 DOM 時,提供多種不同方式的應用轉場效果。包括以下工具:

  • 在 CSS transitionanimation 中自動應用 class 名稱;
  • 也可以配合使用第三方 CSS 動畫庫,例如 Animate.css。
  • 在轉場鉤子函式中使用 JavaScript 直接操作 DOM;
  • 也可以配合使用第三方 JavaScript 動畫庫,例如 Velocity.js。

transition 元件

Vue 提供了 transition 的封裝元件,在下列情形中,可以給任何元素和元件添加進入/離開的轉場效果:

  • 元素或元件初始渲染時
  • 元素或元件顯示/隱藏時(使用 v-ifv-show
  • 元素或元件切換時

先看一個簡單的基本範例:

1
2
3
4
<button v-on:click="show = !show">Toggle</button>
<transition>
  <div class="box" v-if="show">2</div>
</transition>
1
2
3
data: {
  show: true,
},
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.v-enter, .v-leave-to {
  opacity: 0;
}
.v-leave, .v-enter-to {
  opacity: 1;
}

.v-enter-active,
.v-leave-active {
  transition: opacity 1s;
}

當我們使用 transition 元件將元素包住,Vue 會做以下處理:

  1. 自動偵測目標元素是否應用 CSS transition/animation 如果是,在恰當的時機添加/刪除 CSS 類別名稱。
  2. 如果 transition 元件提供了 JavaScript 鉤子函式,這些鉤子函式將在恰當的時機被呼叫。
  3. 如果沒有找到 JavaScript 鉤子並且也沒有檢測到 CSS transition/animation,DOM 操作 (插入/刪除) 在下一幀中立即執行。

1. CSS 轉場的類別名稱

transition 元件共提供了,六種類別名稱提供切換。

進場:

  • v-enter:定義進場的開始狀態。
    • 在元素被插入之前生效,插入之後的下一幀被移除
  • v-enter-active:定義進場生效時的狀態,在整個進場的階段中應用。
    • 在元素被插入之前生效,在 transition/animation 完成之後移除
    • 可用來定義進場的過程時間,延遲和曲線函數
  • v-enter-to2.1.8版及以上 定義進場的結束狀態。
    • 在元素被插入之後下一幀生效(與此同時 v-enter 被移除),在 transition/animation 完成之後移除

離場:

  • v-leave:定義離場的開始狀態。
    • 在離場被觸發時立刻生效,下一幀被移除
  • v-leave-active:定義離場生效時的狀態,在整個離場的階段中應用。
    • 在離場被觸發時立刻生效,在 transition/animation 完成之後移除
    • 這個類可以被用來定義離場的過程時間,延遲和曲線函數
  • v-leave-to2.1.8版及以上 定義離場的結束狀態。
    • 在離開轉場被觸發之後下一幀生效(與此同時 v-leave 被移除),在 transition/animation 完成之後移除

通常會設定 v-enterv-leave-active 相同的效果、v-leavev-enter-to 相同的效果,達到同尾相接形成一個循環,而 v-enter-tov-leave-to 會用來設定轉場時間或動畫。

1.1 CSS transition

transition 元件預設使用上述 v- 前綴的類別名稱。請參考一開始基本範例。

因為不可能只用一種動態效果,因此使用自訂義的前綴,需要加上 name 特性,舉例來說,<transition name="fade">,套用的那麼類別名稱的前綴就會是 fade-enter

1
2
3
4
5
6
<button v-on:click="show = !show">
  Toggle
</button>
<transition name="fade">
  <div class="box" v-if="show"></div>
</transition>

設定一個進場由透明度 0 變 1 並由右測 100px 處滑入,並且進場、離場分別設定不同的曲線函數:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.fade-enter,
.fade-leave-to {
  opacity: 0;
  transform: translateX(100px);
}
.fade-leave, .fade-enter-to {
  opacity: 1;
}

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

再舉一個例子,由下往上進場,由右出場:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.fade-enter {
  opacity: 0;
  transform: translateY(100%);
}

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

.fade-leave-to {
  opacity: 0;
  transform: translateX(100%);
}

.fade-enter-active,.fade-leave-active {
  transition: all 0.8s ease;
}

1.2 CSS animation

CSS animation 用法同 CSS transition,區別是在動畫中 v-enter 類名在節點插入 DOM 後不會立即刪除,而是在 animationend 事件觸發時刪除。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
.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.5);
  }
  100% {
    transform: scale(1);
  }
}

2. 使用自訂義類別名稱

我們可以通過以下 transition 元件特性來使用自訂義類別名稱:

  • enter-class
  • enter-active-class
  • enter-to-class 2.1.8+
  • leave-class
  • leave-active-class
  • leave-to-class 2.1.8+

他們的優先級高於普通的類別名稱。

舉個簡單得範例,來說明使用方式:

1
2
3
4
5
6
7
.opacity-0 {
  opacity: 0;
}

.trs-time-3 {
  transition-duration: 3s;
}
1
2
3
4
5
6
7
8
<transition
  enter-class="opacity-0"
  leave-to-class="opacity-0"
  enter-active-class="trs-time-3"
  leave-active-class="trs-time-3"
>
  <div class="box" v-if="show"></div>
</transition>

2.1 第三方 CSS 動畫庫

自訂義類別名稱,對於套用第三方 CSS 動畫庫結合非常方便。

舉例來說,使用 Animate.css 的動畫效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<button v-on:click="show = !show">
  Toggle
</button>
<transition
  enter-active-class="animated tada"
  leave-active-class="animated bounceOutRight"
>
  <div class="box" v-if="show"></div>
</transition>
<transition
  enter-active-class="animated fadeInUp"
  leave-active-class="animated flipInY"
>
  <div class="box" v-if="show"></div>
</transition>

或者是 magic.css

1
2
3
4
5
6
7
8
9
<button v-on:click="show = !show">
  Toggle
</button>
<transition
  enter-active-class="magictime foolishIn"
  leave-active-class="magictime tinDownOut"
>
  <div class="box" v-if="show"></div>
</transition>

3. 轉場時間

3.1 同時使用 transition/animation

Vue 為了知道轉場的完成,必須設置相應的事件監聽器。它可以是 transitionendanimationend,這取決於給元素應用的 CSS 規則。如果你使用其中任何一種,Vue 能自動識別類型並設置監聽。

但有時候會遇到需要同時設置的情況,但兩邊的完成時間不一致,這時候就需要手動設置 transitionendanimationend 來明確告知 Vue 要監聽的類型。

舉例來說,假設你分別使用自訂義的類別名稱和 Animate.css :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.fade-enter,.fade-leave-to {
  opacity: 0;
}

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

.fade-enter-active,.fade-leave-active {
  transition: opacity 3s;
}
1
2
3
4
5
6
7
8
<transition
  name="fade"
  type="transition"
  enter-active-class="animated swing fade-enter-active"
  leave-active-class="animated bounce fade-leave-active"
>
  <div class="box" v-if="show"></div>
</transition>

這裡要注意,使用自訂義類別名稱優先度大於普通的類別名稱,所以fade-enter-activefade-leave-active 記得要補上。

Animate.css 預設動畫時間為 1s,而我們設置的 transition 時間為 3s,Animate.css 的動畫很外就觸發完成了,而轉場還沒結束。在很多情況下,Vue 可以自動得出轉場效果的完成時機,但還是有可能出現例外。

為了避免出現例外,你就需要手動設定 type="transition"transition 的時間為準。反之動畫時間大於轉場就以動畫時間為準。

3.2 顯性的轉場持續時間

上面已經提過了,在很多情況下,Vue 可以自動得出轉場效果的完成時機。預設情況下,Vue 會等待其在轉場效果的根元素的第一個 transitionendanimationend 事件。

但在某些情況下,還可以自定義整個動態效果總時間長,使用的是 duration 特性搭配 v-bind

舉例來說,總時常為 5s:

1
<transition :duration="5000">...</transition>

也可以分別設定進入和移除持續時間:

1
<transition :duration="{ enter: 500, leave: 800 }">...</transition>

這是一個 transition 需要 5秒,但總轉場時間強制設為 1秒的範例:

4.初始渲染的轉場

透過設定 appear 特性,設置節點在初始渲染的轉場。

1
2
3
<transition appear>
  <!-- ... -->
</transition>

CodePen,載入畫面時,會有進場效果。

appear 預設使用進場的類別,如果要與進場套用不同的動畫效果,需要使用自定義的類別名稱:

1
2
3
<transition appear>
  <!-- ... -->
</transition>

CodePen,載入畫面時,會有進場效果。

appear 預設使用進場的類別,如果要與進場套用不同的動畫效果,需要使用自定義的類別名稱:

1
2
3
4
5
6
7
8
<transition
  appear
  appear-class="custom-appear-class"
  appear-to-class="custom-appear-to-class" (2.1.8+)
  appear-active-class="custom-appear-active-class"
>
  <!-- ... -->
</transition>

多個元素的轉場

<transition> 也可以用在多個元素的轉場。

舉例來說:

1
2
3
4
5
6
<transition>
  <table v-if="items.length > 0">
    <!-- ... -->
  </table>
  <p v-else>Sorry, no items found.</p>
</transition>

但要注意,Vue 在切換畫面時,並不會完全置換元素,會使用一種最大限度減少動態元素並且儘可能的嘗試修復/再利用相同類型元素的算法。

因此當有相同標籤的元素在做切換時,需要使用 key 特性設置唯一的值來標記以讓 Vue 區分它們。

1
2
3
4
5
6
7
8
<transition>
  <button v-if="isEditing" key="save">
    Save
  </button>
  <button v-else key="edit">
    Edit
  </button>
</transition>

在一些場景中,也可以通過給同一個元素的 key 特性設置不同的狀態來代替 v-ifv-else,上面的範例可以重寫為:

1
2
3
4
5
<transition>
  <button v-bind:key="isEditing">
    {{ isEditing ? 'Save' : 'Edit' }}
  </button>
</transition>

使用多個 v-if 的多個元素的轉場可以重寫為綁定了動態屬性的單個元素轉場。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<transition>
  <button v-if="docState === 'saved'" key="saved">
    Edit
  </button>
  <button v-if="docState === 'edited'" key="edited">
    Save
  </button>
  <button v-if="docState === 'editing'" key="editing">
    Cancel
  </button>
</transition>

可以重寫為:

1
2
3
4
5
6
<!-- .html -->
<transition>
  <button :key="docState">
    {{ buttonMessage }}
  </button>
</transition>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ...
computed: {
  buttonMessage: function () {
    switch (this.docState) {
      case 'saved': return 'Edit'
      case 'edited': return 'Save'
      case 'editing': return 'Cancel'
    }
  }
}

1. 轉場模式

請考慮以下程式碼:

1
2
3
4
5
<transition>
  <button :key="isEditing" @click="isEditing = !isEditing">
    {{ isEditing ? 'On' : 'Off' }}
  </button>
</transition>

在 on 按鈕和 off 按鈕的轉場中,兩個按鈕都被重繪了,一個離開轉場的時候另一個開始進入轉場。這是 <transition> 的預設行為,進入和離開同時發生。

你可能會想到,可以加上 position: absolute,因為兩者重疊了,所以不會產生元素位移的情況:

1
2
3
button {
  position: absolute;
}

但如果加上 translate 讓它們運動像滑動,還是會有兩個元素同時出現的破綻:

1
2
3
4
5
.v-enter,
.v-leave-to {
  opacity: 0;
  transform: translateX(100%);
}

同時生效的進入和離開的轉場不能滿足所有要求,所以 Vue 提供了轉場模式:

  • in-out:新元素先進行轉場,完成之後當前元素轉場離開。
  • out-in:當前元素先進行轉場,完成之後新元素轉場進入。

加上 mode="out-in" 的效果:

1
2
3
4
5
<transition mode="out-in">
  <button :key="isEditing" @click="isEditing = !isEditing">
    {{ isEditing ? 'On' : 'Off' }}
  </button>
</transition>
1
2
3
.v-enter-active, .v-leave-active {
  transition: 1.5s opacity;
}

in-out 模式的範例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
button {
  position: absolute;
}

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

.v-enter {
  transform: translateX(100%);
}

.v-leave-to {
  transform: translateX(-100%);
}

多個元件的轉場

多個元件的轉場簡單很多,我們不需要使用 key 特性。只需要使用 動態切換 元件:

1
2
3
4
5
6
7
8
<!-- .html -->
<div id="app">
  <input type="radio" value="v-a" v-model="view" />A
  <input type="radio" value="v-b" v-model="view" />B
  <transition name="component-fade" mode="out-in">
    <component v-bind:is="view"></component>
  </transition>
</div>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.component-fade-enter,
.component-fade-leave-to {
  opacity: 0;
}

.component-fade-enter-active,
.component-fade-leave-active {
  transition: opacity 0.3s ease;
}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const vm = new Vue({
  el: '#app',
  data: {
    view: 'v-a',
  },
  components: {
    'v-a': {
      template: '<div>Component A</div>',
    },
    'v-b': {
      template: '<div>Component B</div>',
    },
  },
});

transition-group 元件

1. 大量元素的轉場

目前為止,都是針對單個節點,或是同一時間渲染多個節點中的一個。如果要針對由 v-for 產生的大量元素(例如:列表),就要改為使用 transition-group 元件。

transition-group 元件有以下幾個特點:

  • 不同於 <transition>,它會以一個真實元素呈現:預設為一個 <span>。可以通過 tag 特性更換為其他元素。
  • 轉場模式(in-outout-in)不可用,因為我們不再相互切換特有的元素。
  • 內部元素 總是需要 提供唯一的 key 屬性值。
  • CSS 轉場的類將會應用在內部的元素中,而不是這個組/容器本身。

1.1 進入/離開的轉場

首先我們看一段程式碼:

1
2
3
4
5
6
7
<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>

<ul>
  <li v-for="item in items" :key="item" class="list-item">
    {{ item }}
</ul>

按下 Add 會隨機插入數字,按下 Remove 會隨機移除數字。

接下來我們改用 transition-group 元件,增加轉場效果。

transition-group 元件預設會有一個 <span> 的根元素,它會包住由 v-for 產生的多個元素當成其子元素。

我們透過 tag 特性,將其改成 <ul>

1
2
3
4
5
<transition-group name="list" tag="ul">
  <li v-for="item in items" v-bind:key="item" class="list-item">
    {{ item }}
  </li>
</transition-group>

並加上 CSS:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
.list-enter,
.list-leave-to {
  opacity: 0;
  transform: translateY(100%);
}

.list-leave,
.list-enter-to {
  opacity: 1;
}

.list-enter-active,
.list-leave-active {
  transition: all 1s;
}

不過這個範例有一個明顯問題,當新增和移除元素的時候,周圍的元素會瞬間移動到他們的新佈局的位置,這會顯得不自然,我們將會在之後解決這個問題。

1.2 排序轉場

transition-group 元件還有一個特殊之處。不僅可以進入和離開動畫,還可以改變定位。

v-move 特性,它會在元素的改變定位的過程中應用。像之前的列別名稱一樣,可以通過 name 屬性來自定義前綴,也可以通過 move-class 屬性手動設置。

v-move 對於設置轉場的切換時機和轉場曲線非常有用。

1
2
3
4
5
6
7
8
9
<button v-on:click="items.reverse()">Reverses</button>

<transition-group name="flip-list" tag="ul">
  <li 
  v-for="item in items"
  :key="item"
  class="list-item">{{ item }}
  </li>
</transition-group>
1
2
3
.flip-list-move {
  transition: transform 1s;
}
1
2
3
4
5
6
const vm = new Vue({
  el: '#app',
  data: {
    items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
  },
});

Vue 使用了一個叫 FLIP 簡單的動畫隊列。使用 transforms 將元素從之前的位置平滑轉場新的位置。

讓我們修正上節不自然的的程式碼:

1
2
3
4
5
6
7
.list-leave-active {
  position: absolute;
}

.list-move {
  transition: 1s;
}

JavaScript 鉤子

transition/animation 的核心概念,就是在對應的時間點新增、移除類別。除了這種在被動的時間點加上類別的方式外,Vue 也提供對應時間的 JavaScript 鉤子讓我們在該時間點使用 JavaScript。

事件鉤子:

  • before-enter:進場前啟動。
  • enter:進場時啟動。
  • after-enter:進場結束後啟動。
  • enter-cancelled:在未完成進場時取消動作。
  • before-leave:離場前啟動。
  • leave:離場時啟動。
  • after-leave:離場結束後啟動。
  • leave-cancelled:在未完成離場時取消動作。(只用於 v-show 中)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<transition
  v-on:before-enter="beforeEnter"
  v-on:enter="enter"
  v-on:after-enter="afterEnter"
  v-on:enter-cancelled="enterCancelled"

  v-on:before-leave="beforeLeave"
  v-on:leave="leave"
  v-on:after-leave="afterLeave"
  v-on:leave-cancelled="leaveCancelled"
>
</transition>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
methods: {
  beforeEnter: function(el) {},
  enter: function(el, done) {
    done();
  },
  afterEnter: function(el) {},
  enterCancelled: function(el) {},

  beforeLeave: function(el) {},
  leave: function(el, done) {
    done();
  },
  afterLeave: function(el) {},
  leaveCancelled: function(el) {},  
}
  • 如果只要單獨使用 JS 鉤子轉場,可以設置 v-bind:css="false",Vue 會跳過 CSS 的檢測,避免轉場過程受到 CSS 的影響。
  • enterleave 的回呼函式 done 是可選的:
    • 參數不注入 done 時,預設為同步呼叫,會自動呼叫 done()
    • 如果有 done 參數,就需要手動呼叫 done(),沒呼叫就不會進入 after-enterafter-leave
    • 如果想要搭配 CSS 同步自動 call done 的話,就不要帶 done 參數
    • 只使用 JavaScript 鉤子的時候,要記得呼叫 done()

初始渲染也有事件鉤子:

  • before-appear:載入前。
  • appear:載入時。
  • after-appear:載入後。
  • appear-cancelled:載入開始後,執行取消載入。

路由轉場

<router-view> 一樣可以使用 <transition> 元件:

1
2
3
<transition>
  <router-view></router-view>
</transition>

1. 單個路由的轉場

如果要讓每個路由元件有各自的轉場效果,可以在元件內使用 <transition> 元件並設置不同的 name

1
2
3
4
5
6
<!-- Views1.vue -->
<template >
  <transition name="slide">
    ...
  </transition>
</template>
1
2
3
4
5
6
<!-- Views2.vue -->
<template >
  <transition name="fade">
    ...
  </transition>
</template>

2. 動態轉場

1
2
3
<transition :name="transitionName">
  <router-view></router-view>
</transition>

在接著在父元件內,監聽 $route 決定使用哪種轉場效果:

1
2
3
4
5
6
7
watch: {
  '$route' (to, from) {
    const toDepth = to.path.split('/').length
    const fromDepth = from.path.split('/').length
    this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
  }
}

3. Vueg

Vueg
為 vue-router 添加轉場效果的開源 Vue 套件。

交錯轉場

這裡有一段程式碼:

1
2
3
4
5
6
<button @click="show = !show">show</button>
<ul v-if="show">
  <li v-for="n in num" :key="n">
    {{ n }}
  </li>
</ul>
1
2
3
4
data: {
  num: 5,
  show: false,
},

我們要如何將它改成交錯轉場?

1. transition-delay

最簡單的方式,就是透過 transition-delay 來實現列表的交錯轉場。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.custom-appear-active-class {
  transition: opacity 0.3s;
}

.custom-appear-active-class:nth-child(5n + 2) {
  transition-delay: 0.3s;
}
.custom-appear-active-class:nth-child(5n + 3) {
  transition-delay: 0.5s;
}
.custom-appear-active-class:nth-child(5n + 4) {
  transition-delay: 0.7s;
}
.custom-appear-active-class:nth-child(5n + 5) {
  transition-delay: 0.9s;
} 

但缺點也很明顯,要寫大量的 CSS,修改起來也不靈活。

2. 透過 data 屬性與 JavaScript 交流

透過 data 屬性取得元素的 index,所以補上 :data-index = index

1
2
3
4
5
6
7
8
9
  <transition-group tag="ul"
    v-if="show"
    v-bind:css="false"
    @appear="appear"
    appear >
    <li v-for="(n,index) in num" :key="n" :data-index = index >
      {{ n }}
    </li>
  </transition-group>

利用 el.dataset 取取得元素的 data 屬性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
methods:{
  appear(el, done) {
    const index = el.dataset.index;  // 取得 index
    const delay = index * 0.3;
    gsap.from(el,{
      duration: 0.3,
      opacity: 0,
      delay: delay,
    })
    done();
  },
},

我這裡使用 GSAP3 動畫來設置動畫。

或是使用 Velocity.js 搭配 setTimeout

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
methods:{
  beforeAppear(el) {
    el.style.opacity = 0;
  },
  appear(el, done) {
    const index = el.dataset.index;
    const delay = index * 300;
    setTimeout(function () {
      Velocity(
        el,
        { opacity: 1 },
        { complete: done }
      );
    }, delay);
  },
},

竹白
作者
竹白
前端筆記

文章目錄