列表过渡

  • 单个节点
  • 同一时间渲染多个节点中的一个

那么怎么同时渲染整个列表,比如使用 ?在这种场景中,使用 <transition-group> 组件。在我们深入例子之前,先了解关于这个组件的几个特点:

  • 不同于 <transition>,它会以一个真实元素渲染:默认为一个 <span>。你也可以通过 tag attribute 更换为其他元素。
  • 不可用,因为我们不再相互切换特有的元素。
  • 内部元素总是需要提供唯一的 key attribute 值。
  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。

现在让我们由一个简单的例子深入,进入和离开的过渡使用之前一样的 CSS class 名。

  1. const Demo = {
  2. data() {
  3. return {
  4. items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
  5. nextNum: 10
  6. }
  7. },
  8. methods: {
  9. randomIndex() {
  10. return Math.floor(Math.random() * this.items.length)
  11. },
  12. add() {
  13. this.items.splice(this.randomIndex(), 0, this.nextNum++)
  14. },
  15. remove() {
  16. this.items.splice(this.randomIndex(), 1)
  17. }
  18. }
  19. }
  20. Vue.createApp(Demo).mount('#list-demo')
  1. .list-item {
  2. display: inline-block;
  3. margin-right: 10px;
  4. }
  5. .list-enter-active,
  6. .list-leave-active {
  7. transition: all 1s ease;
  8. }
  9. .list-enter-from,
  10. .list-leave-to {
  11. opacity: 0;
  12. transform: translateY(30px);
  13. }

这个例子有个问题,当添加和移除元素的时候,周围的元素会瞬间移动到他们的新布局的位置,而不是平滑的过渡,我们下面会解决这个问题。

<transition-group> 组件还有一个特殊之处。不仅可以进入和离开动画,还可以改变定位。要使用这个新功能只需了解新增的 v-move class,它会在元素的改变定位的过程中应用。像之前的类名一样,可以通过 name attribute 来自定义前缀,也可以通过 move-class attribute 手动设置。

该 class 主要用于指定过渡 timing 和 easing 曲线,如下所示:

  1. <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
  2. <div id="flip-list-demo">
  3. <button @click="shuffle">Shuffle</button>
  4. <transition-group name="flip-list" tag="ul">
  5. <li v-for="item in items" :key="item">
  6. {{ item }}
  7. </li>
  8. </transition-group>
  9. </div>
  1. const Demo = {
  2. data() {
  3. return {
  4. items: [1, 2, 3, 4, 5, 6, 7, 8, 9]
  5. }
  6. },
  7. methods: {
  8. shuffle() {
  9. this.items = _.shuffle(this.items)
  10. }
  11. }
  12. }
  13. Vue.createApp(Demo).mount('#flip-list-demo')

这个看起来很神奇,内部的实现,Vue 使用了一个叫 FLIP (opens new window) 简单的动画队列使用 transforms 将元素从之前的位置平滑过渡新的位置。

  1. <div id="list-complete-demo" class="demo">
  2. <button @click="add">Add</button>
  3. <button @click="remove">Remove</button>
  4. <transition-group name="list-complete" tag="p">
  5. <span v-for="item in items" :key="item" class="list-complete-item">
  6. {{ item }}
  7. </span>
  8. </transition-group>
  9. </div>
  1. const Demo = {
  2. data() {
  3. return {
  4. items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
  5. nextNum: 10
  6. }
  7. },
  8. methods: {
  9. randomIndex() {
  10. return Math.floor(Math.random() * this.items.length)
  11. },
  12. add() {
  13. this.items.splice(this.randomIndex(), 0, this.nextNum++)
  14. },
  15. remove() {
  16. this.items.splice(this.randomIndex(), 1)
  17. },
  18. shuffle() {
  19. this.items = _.shuffle(this.items)
  20. }
  21. }
  22. }
  23. Vue.createApp(Demo).mount('#list-complete-demo')
  1. .list-complete-item {
  2. transition: all 0.8s ease;
  3. display: inline-block;
  4. margin-right: 10px;
  5. }
  6. .list-complete-enter-from,
  7. .list-complete-leave-to {
  8. opacity: 0;
  9. transform: translateY(30px);
  10. }
  11. .list-complete-leave-active {
  12. position: absolute;
  13. }

TIP

需要注意的是使用 FLIP 过渡的元素不能设置为 display: inline。作为替代方案,可以设置为 display: inline-block 或者放置于 flex 中

FLIP 动画不仅可以实现单列过渡,多维网格也:

TODO:示例

通过 data attribute 与 JavaScript 通信,就可以实现列表的交错过渡:

  1. <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js"></script>
  2. <div id="demo">
  3. <input v-model="query" />
  4. <transition-group
  5. name="staggered-fade"
  6. tag="ul"
  7. :css="false"
  8. @before-enter="beforeEnter"
  9. @enter="enter"
  10. @leave="leave"
  11. >
  12. <li
  13. v-for="(item, index) in computedList"
  14. :key="item.msg"
  15. :data-index="index"
  16. >
  17. {{ item.msg }}
  18. </li>
  19. </div>

过渡可以通过 Vue 的组件系统实现复用。要创建一个可复用过渡组件,你需要做的就是将 或者 <transition-group> 作为根组件,然后将任何子组件放置在其中就可以了。

使用 template 的简单例子:

  1. Vue.component('my-special-transition', {
  2. template: '\
  3. <transition\
  4. name="very-special-transition"\
  5. mode="out-in"\
  6. @before-enter="beforeEnter"\
  7. @after-enter="afterEnter"\
  8. >\
  9. <slot></slot>\
  10. </transition>\
  11. ',
  12. methods: {
  13. beforeEnter(el) {
  14. // ...
  15. },
  16. afterEnter(el) {
  17. // ...
  18. }
  19. }
  20. })

函数式组件更适合完成这个任务:

  1. Vue.component('my-special-transition', {
  2. functional: true,
  3. render: function(createElement, context) {
  4. var data = {
  5. props: {
  6. name: 'very-special-transition',
  7. mode: 'out-in'
  8. },
  9. on: {
  10. beforeEnter(el) {
  11. // ...
  12. },
  13. afterEnter(el) {
  14. // ...
  15. }
  16. }
  17. }
  18. return createElement('transition', data, context.children)
  19. }
  20. })

动态过渡

在 Vue 中即使是过渡也是数据驱动的!动态过渡最基本的例子是通过 name attribute 来绑定动态值。

  1. <transition :name="transitionName">
  2. <!-- ... -->
  3. </transition>

当你想用 Vue 的过渡系统来定义的 CSS 过渡/动画在不同过渡间切换会非常有用。

所有过渡 attribute 都可以动态绑定,但我们不仅仅只有 attribute 可以利用,还可以通过事件钩子获取上下文中的所有数据,因为事件钩子都是方法。这意味着,根据组件的状态不同,你的 JavaScript 过渡会有不同的表现

  1. <script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
  2. <div id="dynamic-fade-demo" class="demo">
  3. Fade In:
  4. <input type="range" v-model="fadeInDuration" min="0" :max="maxFadeDuration" />
  5. Fade Out:
  6. <input
  7. type="range"
  8. v-model="fadeOutDuration"
  9. min="0"
  10. :max="maxFadeDuration"
  11. />
  12. <transition
  13. :css="false"
  14. @before-enter="beforeEnter"
  15. @enter="enter"
  16. @leave="leave"
  17. >
  18. <p v-if="show">hello</p>
  19. </transition>
  20. <button v-if="stop" @click="stop = false; show = false">
  21. Start animating
  22. </button>
  23. <button v-else @click="stop = true">Stop it!</button>
  24. </div>

TODO:示例