# 进入过渡 & 离开过渡

在插入、更新或从 DOM 中移除项时,Vue 提供了多种应用转换效果的方法。这包括以下工具:

  • 自动为 CSS 过渡和动画应用 class;
  • 集成第三方 CSS 动画库,例如 animate.css(opens new window)
  • 在过渡钩子期间使用 JavaScript 直接操作 DOM;
  • 集成第三方 JavaScript 动画库。

在这里,我们只介绍进入、离开的过渡,你也可以从下一节中学习列表过渡管理过渡状态

# 单元素/组件的过渡

Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡

  • 条件渲染 (使用 v-if)
  • 条件展示 (使用 v-show)
  • 动态组件
  • 组件根节点

这里是一个典型的例子:

<div id="demo">
  <button @click="show = !show">
    Toggle
  </button>

  <transition name="fade">
    <p v-if="show">hello</p>
  </transition>
</div>
1
2
3
4
5
6
7
8
9
const Demo = {
  data() {
    return {
      show: true
    }
  }
}

Vue.createApp(Demo).mount('#demo')
1
2
3
4
5
6
7
8
9
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

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

See the Pen Simple Transition Component by Vue (@Vue) on CodePen.

当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理:

  1. 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。

  2. 如果过渡组件提供了 JavaScript 钩子函数 ,这些钩子函数将在恰当的时机被调用。

  3. 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。(注意:此处指浏览器逐帧动画机制,和 Vue 的 nextTick 概念不同)

# 过渡 class

在进入/离开的过渡中,会有 6 个 class 切换。

  1. v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。

  2. v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。

  3. v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。

  4. v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。

  5. v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。

  6. v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被移除),在过渡/动画完成之后移除。

Transition Diagram

这里的每个 class 都将以过渡的名字添加前缀。如果你使用了一个没有名字的 <transition>,则 v- 是这些 class 名的默认前缀。举例来说,如果你使用了 <transition name="my-transition">,那么 v-enter-from 会替换为 my-transition-enter-from

v-enter-activev-leave-active 可以控制进入/离开过渡的不同的缓和曲线,在下面章节会有个示例说明。

# CSS 过渡

CSS 过渡是最常用的过渡类型之一。举例:

<div id="demo">
  <button @click="show = !show">
    Toggle render
  </button>

  <transition name="slide-fade">
    <p v-if="show">hello</p>
  </transition>
</div>
1
2
3
4
5
6
7
8
9
const Demo = {
  data() {
    return {
      show: true
    }
  }
}

Vue.createApp(Demo).mount('#demo')
1
2
3
4
5
6
7
8
9
/* 可以为进入和离开动画设置不同的持续时间和动画函数 */
.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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

See the Pen Different Enter and Leave Transitions by Vue (@Vue) on CodePen.

# CSS 动画

CSS 动画用法同 CSS 过渡,区别是在动画中 v-enter-from 类在节点插入 DOM 后不会立即移除,而是在 animationend 事件触发时移除。

下面是一个例子,为了简洁起见,省略了带前缀的 CSS 规则:

<div id="demo">
  <button @click="show = !show">Toggle show</button>
  <transition name="bounce">
    <p v-if="show">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis
      enim libero, at lacinia diam fermentum id. Pellentesque habitant morbi
      tristique senectus et netus.
    </p>
  </transition>
</div>
1
2
3
4
5
6
7
8
9
10
const Demo = {
  data() {
    return {
      show: true
    }
  }
}

Vue.createApp(Demo).mount('#demo')
1
2
3
4
5
6
7
8
9
.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);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

See the Pen CSS Animation Transition Example by Vue (@Vue) on CodePen.

# 自定义过渡 class 类名

我们可以通过以下 attribute 来自定义过渡类名:

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

它们的优先级高于普通的类名,当你希望将其它第三方 CSS 动画库与 Vue 的过度系统相结合时十分有用,比如 Animate.css(opens new window)

示例:

<link
  href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.0/animate.min.css"
  rel="stylesheet"
  type="text/css"
/>

<div id="demo">
  <button @click="show = !show">
    Toggle render
  </button>

  <transition
    name="custom-classes-transition"
    enter-active-class="animate__animated animate__tada"
    leave-active-class="animate__animated animate__bounceOutRight"
  >
    <p v-if="show">hello</p>
  </transition>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const Demo = {
  data() {
    return {
      show: true
    }
  }
}

Vue.createApp(Demo).mount('#demo')
1
2
3
4
5
6
7
8
9

# 同时使用过渡和动画

Vue 为了知道过渡何时完成,必须设置相应的事件监听器。它可以是 transitionendanimationend,这取决于给元素应用的 CSS 规则。如果你只使用了其中一种,Vue 能自动识别其正确类型。

但是,在一些场景中,你需要给同一个元素同时设置两种过渡动效,比如有一个通过 Vue 触发的 CSS 动画,并且在悬停时结合一个 CSS 过渡。在这种情况中,你就需要使用 type attribute 并设置 animationtransition 来显式声明你需要 Vue 监听的类型。

# 显性的过渡持续时间

在大多数情况下,Vue 可以自动得出过渡效果的完成时机。默认情况下,Vue 会等待其在过渡效果的根元素的第一个 transitionendanimationend 事件。然而,有时候这也许不是预期的行为——比如,我们也许拥有一个精心编排的一系列过渡效果,其中一些嵌套的内部元素相比于过渡效果的根元素具有延迟的或更长的过渡效果。

在这种情况下你可以用 <transition> 组件上的 duration prop 显式指定过渡持续时间 (以毫秒计):

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

你也可以分别指定进入和离开的持续时间:

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

# JavaScript 钩子

可以在 attribute 中声明 JavaScript 钩子:

<transition
  @before-enter="beforeEnter"
  @enter="enter"
  @after-enter="afterEnter"
  @enter-cancelled="enterCancelled"
  @before-leave="beforeLeave"
  @leave="leave"
  @after-leave="afterLeave"
  @leave-cancelled="leaveCancelled"
  :css="false"
>
  <!-- ... -->
</transition>
1
2
3
4
5
6
7
8
9
10
11
12
13
// ...
methods: {
  // --------
  // 进入时
  // --------

  beforeEnter(el) {
    // ...
  },
  // 当与 CSS 结合使用时
  // 回调函数 done 是可选的
  enter(el, done) {
    // ...
    done()
  },
  afterEnter(el) {
    // ...
  },
  enterCancelled(el) {
    // ...
  },

  // --------
  // 离开时
  // --------

  beforeLeave(el) {
    // ...
  },
  // 当与 CSS 结合使用时
  // 回调函数 done 是可选的
  leave(el, done) {
    // ...
    done()
  },
  afterLeave(el) {
    // ...
  },
  // leaveCancelled 只用于 v-show 中
  leaveCancelled(el) {
    // ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

这些钩子函数可以结合 CSS transitions/animations 使用,也可以单独使用。

当只用 JavaScript 过渡的时候,在 enterleave 钩子中必须使用 done 进行回调。否则,它们将被同步调用,过渡会立即完成。添加 :css="false" 也会让 Vue 会跳过 CSS 的检测,除了性能略高之外,这也可以避免过渡过程中受到 CSS 规则的意外影响。

现在让我们来看一个例子。下面是一个使用 GreenSock(opens new window) 的 JavaScript 过渡:

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js"></script>

<div id="demo">
  <button @click="show = !show">
    Toggle
  </button>

  <transition
    @before-enter="beforeEnter"
    @enter="enter"
    @leave="leave"
    :css="false"
  >
    <p v-if="show">
      Demo
    </p>
  </transition>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Demo = {
  data() {
    return {
      show: false
    }
  },
  methods: {
    beforeEnter(el) {
      gsap.set(el, {
        scaleX: 0.8,
        scaleY: 1.2
      })
    },
    enter(el, done) {
      gsap.to(el, {
        duration: 1,
        scaleX: 1.5,
        scaleY: 0.7,
        opacity: 1,
        x: 150,
        ease: 'elastic.inOut(2.5, 1)',
        onComplete: done
      })
    },
    leave(el, done) {
      gsap.to(el, {
        duration: 0.7,
        scaleX: 1,
        scaleY: 1,
        x: 300,
        ease: 'elastic.inOut(2.5, 1)'
      })
      gsap.to(el, {
        duration: 0.2,
        delay: 0.5,
        opacity: 0,
        onComplete: done
      })
    }
  }
}

Vue.createApp(Demo).mount('#demo')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

See the Pen JavaScript Hooks Transition by Vue (@Vue) on CodePen.

# 初始渲染的过渡

可以通过 appear attribute 设置节点在初始渲染的过渡:

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

# 多个元素之间的过渡

我们将在之后讨论多个组件之间的过渡,但是你也可以通过 v-if/v-else 来完成元素之间的过渡。最常见的多标签过渡是一个列表和描述这个列表为空消息的元素:

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

实际上,通过使用 v-if/v-else-if/v-else 或将单个元素绑定到一个动态 property,可以在任意数量的元素之间进行过渡。例如:

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

也可以写为:

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

# 过渡模式

这里还有一个问题,试着点击下面的按钮:

See the Pen Transition Modes Button Problem by Vue (@Vue) on CodePen.

在“on”按钮和“off”按钮的过渡过程中,两个按钮都被绘制了——其中一个离开过渡的时候另一个开始进入过渡。这是 <transition> 的默认行为——进入和离开同时发生。

有时这很有效,例如当过渡项使用绝对定位到顶端时:

See the Pen Transition Modes Button Problem- positioning by Vue (@Vue) on CodePen.

有时候这并不是一个可行的选项,或者说我们正在处理更复杂的动作,需要协调进入和离开的状态,所以 Vue 提供了一个非常有用的工具,称之为过渡模式

  • in-out: 新元素先进行进入过渡,完成之后当前元素过渡离开。
  • out-in: 当前元素先进行离开过渡,完成之后新元素过渡进入。

TIP

你很快就会发现 out-in 是你大多数时候想要的状态 😃

现在让我们用 out-in 更新 on/off 按钮的转换:

<transition name="fade" mode="out-in">
  <!-- ... the buttons ... -->
</transition>
1
2
3

See the Pen Transition Modes Button Problem- solved by Vue (@Vue) on CodePen.

通过添加一个 attribute,我们修复了原来的过渡,而不必添加任何特殊样式。

我们可以用它来协调更具表现力的动作,例如折叠卡片,如下所示。这实际上是两个元素在彼此之间转换,但是由于开始状态和结束状态的比例是相同的:水平方向上为 0,它看起来就运动得非常连贯。这种技巧对于实现拟真的 UI 交互细节非常有用:

See the Pen Transition Modes Flip Cards by Vue (@Vue) on CodePen.

# 多个组件之间的过渡

组件之间的过渡更简单——我们甚至不需要 key 属性。取而代之的是,我们包裹了一个动态组件

<div id="demo">
  <input v-model="view" type="radio" value="v-a" id="a"><label for="a">A</label>
  <input v-model="view" type="radio" value="v-b" id="b"><label for="b">B</label>
  <transition name="component-fade" mode="out-in">
    <component :is="view"></component>
  </transition>
</div>
1
2
3
4
5
6
7
const Demo = {
  data() {
    return {
      view: 'v-a'
    }
  },
  components: {
    'v-a': {
      template: '<div>Component A</div>'
    },
    'v-b': {
      template: '<div>Component B</div>'
    }
  }
}

Vue.createApp(Demo).mount('#demo')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.component-fade-enter-active,
.component-fade-leave-active {
  transition: opacity 0.3s ease;
}

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

See the Pen Transitioning between components by Vue (@Vue) on CodePen.