VueJS 列表过渡

  • 定义概述

    在上一章节关于过渡我们已经讲到:
    • 单个节点
    • 同一时间渲染多个节点中的一个
    那么怎么同时渲染整个列表,比如使用 v-for ?在这种场景中,使用 <transition-group> 组件。在我们深入例子之前,先了解关于这个组件的几个特点:
    包括以下工具:
    • 不同于 <transition>,它会以一个真实元素呈现:默认为一个 <span>。你也可以通过 tag 特性更换为其他元素。
    • 过渡模式 不可用,因为我们不再相互切换特有的元素。
    • 内部元素 总是需要 提供唯一的 key 属性值。
    • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。
  • 列表的进入/离开过渡

    现在让我们由一个简单的例子深入,进入和离开的过渡使用之前一样的 CSS 类名。
    <div id="list-demo" class="demo">
         <button v-on:click="add">Add</button>
         <button v-on:click="remove">Remove</button>
         <transition-group name="list" tag="p">
           <span v-for="item in items" v-bind:key="item" class="list-item">
             {{ item }}
           </span>
         </transition-group>
     </div>
     <script>
     new Vue({
       el: '#list-demo',
       data: {
         items: [1,2,3,4,5,6,7,8,9],
         nextNum: 10
       },
       methods: {
         randomIndex: function () {
           return Math.floor(Math.random() * this.items.length)
         },
         add: function () {
           this.items.splice(this.randomIndex(), 0, this.nextNum++)
         },
         remove: function () {
           this.items.splice(this.randomIndex(), 1)
         },
       }
     })
     </script>
     <style>
     .list-item {
       display: inline-block;
       margin-right: 10px;
     }
     .list-enter-active, .list-leave-active {
       transition: all 1s;
     }
     .list-enter, .list-leave-to{
       opacity: 0;
       transform: translateY(30px);
     }
     </style>
    
    尝试一下
    这个例子有个问题,当添加和移除元素的时候,周围的元素会瞬间移动到他们的新布局的位置,而不是平滑的过渡,我们下面会解决这个问题。
  • 列表的进入/离开过渡

    <transition-group> 组件还有一个特殊之处。不仅可以进入和离开动画,还可以改变定位。要使用这个新功能只需了解新增的 v-move 特性,它会在元素的改变定位的过程中应用。像之前的类名一样,可以通过 name 属性来自定义前缀,也可以通过 move-class 属性手动设置。
    v-move 对于设置过渡的切换时机和过渡曲线非常有用,你会看到如下的例子:
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>
     
     <div id="flip-list-demo" class="demo">
       <button v-on:click="shuffle">Shuffle</button>
       <transition-group name="flip-list" tag="ul">
         <li v-for="item in items" v-bind:key="item">
           {{ item }}
         </li>
       </transition-group>
     </div>
     <script>
     new Vue({
       el: '#flip-list-demo',
       data: {
         items: [1,2,3,4,5,6,7,8,9]
       },
       methods: {
         shuffle: function () {
           this.items = _.shuffle(this.items)
         }
       }
     })
     </script>
     <style>
     .flip-list-move {
       transition: transform 1s;
     }
     </style>
    
    尝试一下
    这个看起来很神奇,内部的实现,Vue 使用了一个叫 FLIP 简单的动画队列使用 transforms 将元素从之前的位置平滑过渡新的位置。
    我们将之前实现的例子和这个技术结合,使我们列表的一切变动都会有动画过渡。
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>
     <div id="list-complete-demo" class="demo">
       <button v-on:click="shuffle">Shuffle</button>
       <button v-on:click="add">Add</button>
       <button v-on:click="remove">Remove</button>
       <transition-group name="list-complete" tag="p">
         <span
           v-for="item in items"
           v-bind:key="item"
           class="list-complete-item"
         >
           {{ item }}
         </span>
       </transition-group>
     </div>
     <script>
     new Vue({
       el: '#list-complete-demo',
       data: {
         items: [1,2,3,4,5,6,7,8,9],
         nextNum: 10
       },
       methods: {
         randomIndex: function () {
           return Math.floor(Math.random() * this.items.length)
         },
         add: function () {
           this.items.splice(this.randomIndex(), 0, this.nextNum++)
         },
         remove: function () {
           this.items.splice(this.randomIndex(), 1)
         },
         shuffle: function () {
           this.items = _.shuffle(this.items)
         }
       }
     })
     </script>
     <style>
     .list-complete-item {
       transition: all 1s;
       display: inline-block;
       margin-right: 10px;
     }
     .list-complete-enter, .list-complete-leave-to {
       opacity: 0;
       transform: translateY(30px);
     }
     .list-complete-leave-active {
       position: absolute;
     }
     </style>
    
    尝试一下
    需要注意的是使用 FLIP 过渡的元素不能设置为 display: inline 。作为替代方案,可以设置为 display: inline-block 或者放置于 flex 中
  • 列表的交错过渡

    通过 data 属性与 JavaScript 通信 ,就可以实现列表的交错过渡:
    <script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
     <div id="staggered-list-demo">
       <input v-model="query">
       <transition-group
         name="staggered-fade"
         tag="ul"
         v-bind:css="false"
         v-on:before-enter="beforeEnter"
         v-on:enter="enter"
         v-on:leave="leave"
       >
         <li
           v-for="(item, index) in computedList"
           v-bind:key="item.msg"
           v-bind:data-index="index"
         >{{ item.msg }}</li>
       </transition-group>
     </div>
     <script>
     new Vue({
       el: '#staggered-list-demo',
       data: {
         query: '',
         list: [
           { msg: 'Bruce Lee' },
           { msg: 'Jackie Chan' },
           { msg: 'Chuck Norris' },
           { msg: 'Jet Li' },
           { msg: 'Kung Fury' }
         ]
       },
       computed: {
         computedList: function () {
           var vm = this
           return this.list.filter(function (item) {
             return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1
           })
         }
       },
       methods: {
         beforeEnter: function (el) {
           el.style.opacity = 0
           el.style.height = 0
         },
         enter: function (el, done) {
           var delay = el.dataset.index * 150
           setTimeout(function () {
             Velocity(
               el,
               { opacity: 1, height: '1.6em' },
               { complete: done }
             )
           }, delay)
         },
         leave: function (el, done) {
           var delay = el.dataset.index * 150
           setTimeout(function () {
             Velocity(
               el,
               { opacity: 0, height: 0 },
               { complete: done }
             )
           }, delay)
         }
       }
     })
     </script>
    
    尝试一下
  • 可复用的过渡

    过渡可以通过 Vue 的组件系统实现复用。要创建一个可复用过渡组件,你需要做的就是将 <transition> 或者 <transition-group> 作为根组件,然后将任何子组件放置在其中就可以了。
    使用 template 的简单例子:
    Vue.component('my-special-transition', {
       template: '\
         <transition\
           name="very-special-transition"\
           mode="out-in"\
           v-on:before-enter="beforeEnter"\
           v-on:after-enter="afterEnter"\
         >\
            <slot></slot>\
          </transition>\
       ',
       methods: {
         beforeEnter: function (el) {
           // ...
         },
         afterEnter: function (el) {
           // ...
         }
       }
     })
    
    函数式组件更适合完成这个任务:
    Vue.component('my-special-transition', {
       functional: true,
       render: function (createElement, context) {
         var data = {
           props: {
             name: 'very-special-transition',
             mode: 'out-in'
           },
           on: {
             beforeEnter: function (el) {
               // ...
             },
             afterEnter: function (el) {
               // ...
             }
           }
         }
         return createElement('transition', data, context.children)
       }
     })
    
  • 动态过渡

    Vue 中即使是过渡也是数据驱动的!动态过渡最基本的例子是通过 name 特性来绑定动态值。
    <transition v-bind:name="transitionName">
         
     </transition>
    
    当你想用 Vue 的过渡系统来定义的 CSS 过渡/动画 在不同过渡间切换会非常有用。
    所有过渡特性都可以动态绑定,但我们不仅仅只有特性可以利用,还可以通过事件钩子获取上下文中的所有数据,因为事件钩子都是方法。这意味着,根据组件的状态不同,你的 JavaScript 过渡会有不同的表现。
    <script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
     <div id="dynamic-fade-demo" class="demo">
       Fade In: <input type="range" v-model="fadeInDuration" min="0" v-bind:max="maxFadeDuration">
       Fade Out: <input type="range" v-model="fadeOutDuration" min="0" v-bind:max="maxFadeDuration">
       <transition
         v-bind:css="false"
         v-on:before-enter="beforeEnter"
         v-on:enter="enter"
         v-on:leave="leave"
       >
         <p v-if="show">hello</p>
       </transition>
       <button
         v-if="stop"
         v-on:click="stop = false; show = false"
       >Start animating</button>
       <button
         v-else
         v-on:click="stop = true"
       >Stop it!</button>
     </div>
     <script>
     new Vue({
       el: '#dynamic-fade-demo',
       data: {
         show: true,
         fadeInDuration: 1000,
         fadeOutDuration: 1000,
         maxFadeDuration: 1500,
         stop: true
       },
       mounted: function () {
         this.show = false
       },
       methods: {
         beforeEnter: function (el) {
           el.style.opacity = 0
         },
         enter: function (el, done) {
           var vm = this
           Velocity(el,
             { opacity: 1 },
             {
               duration: this.fadeInDuration,
               complete: function () {
                 done()
                 if (!vm.stop) vm.show = false
               }
             }
           )
         },
         leave: function (el, done) {
           var vm = this
           Velocity(el,
             { opacity: 0 },
             {
               duration: this.fadeOutDuration,
               complete: function () {
                 done()
                 vm.show = true
               }
             }
           )
         }
       }
     })
     </script>
    
    尝试一下
    最后,创建动态过渡的最终方案是组件通过接受 props 来动态修改之前的过渡。一句老话,唯一的限制是你的想象力。