目前为止,关于过渡我们已经介绍了:
那么怎么同时渲染整个列表,使用 v-for
?
在这种场景下,我们可以使用Vue自带的 <transition-group>
组件。
在深入例子之前,先了解这个组件的几个特点:
tag
属性指定渲染为什么包裹元素。key
属性值。看下面的例子,它的进入和离开的过渡使用之前一样的 CSS 类名。
<div id="list-demo"> <button @click="add">Add</button> <button @click="remove">Remove</button> <transition-group name="list" tag="p"> <span v-for="item in items" :key="item" class="list-item"> {{ item }} </span> </transition-group> </div>
可以在浏览器中观察最终渲染出来的代码。
const Demo = { data() { return { items: [1, 2, 3, 4, 5, 6, 7, 8, 9], nextNum: 10 } }, methods: { randomIndex() { return Math.floor(Math.random() * this.items.length) }, add() { this.items.splice(this.randomIndex(), 0, this.nextNum++) }, remove() { this.items.splice(this.randomIndex(), 1) } } } Vue.createApp(Demo).mount('#list-demo')
.list-item { display: inline-block; margin-right: 10px; } .list-enter-active, .list-leave-active { transition: all 1s ease; } .list-enter-from, .list-leave-to { opacity: 0; transform: translateY(30px); }
这个例子的效果有点小瑕疵,当添加和移除元素的时候,周围的元素会瞬间移动到他们的新布局的位置,而不是平滑的过渡,我们下面会解决这个问题。
<transition-group>
组件还有一个特殊之处。不仅可以进入和离开动画,还可以改变定位。
要使用这个新功能需要了解新的 v-move
类,它会在元素的改变定位的过程中应用。
其中的v,用name属性值替换。
这个类主要用于指定过渡 timing 和 easing 曲线,如下所示:
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script> <div id="flip-list-demo"> <button @click="shuffle">Shuffle</button> <transition-group name="flip-list" tag="ul"> <li v-for="item in items" :key="item"> {{ item }} </li> </transition-group> </div>
导入lodash库的目的是使用它的shuffle功能。
const Demo = { data() { return { items: [1, 2, 3, 4, 5, 6, 7, 8, 9] } }, methods: { shuffle() { this.items = _.shuffle(this.items) } } } Vue.createApp(Demo).mount('#flip-list-demo')
.flip-list-move { transition: transform 0.8s ease; }
注意上面的CSS代码中的类名。
Vue 内部使用 FLIP 动画技术,它使用 transform 将元素从之前的位置平滑过渡新的位置。
其实我们直接使用transform特效也可以达到同样的效果,修改一下最开始的例子:
<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 @click="shuffle">Shuffle</button> <button @click="add">Add</button> <button @click="remove">Remove</button> <transition-group name="list-complete" tag="p"> <span v-for="item in items" :key="item" class="list-complete-item"> {{ item }} </span> </transition-group> </div>
const Demo = { data() { return { items: [1, 2, 3, 4, 5, 6, 7, 8, 9], nextNum: 10 } }, methods: { randomIndex() { return Math.floor(Math.random() * this.items.length) }, add() { this.items.splice(this.randomIndex(), 0, this.nextNum++) }, remove() { this.items.splice(this.randomIndex(), 1) }, shuffle() { this.items = _.shuffle(this.items) } } } Vue.createApp(Demo).mount('#list-complete-demo')
.list-complete-item { transition: all 0.8s ease; display: inline-block; margin-right: 10px; } .list-complete-enter-from, .list-complete-leave-to { opacity: 0; transform: translateY(30px); } .list-complete-leave-active { position: absolute; }
通过 data 属性与JS通信,就可以实现列表的交错过渡。
下面的例子有一个列表和一个搜索框,在搜索框中输入关键字,过滤包换关键字的列表项:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js"></script> <div id="demo"> <input v-model="query" /> <transition-group name="staggered-fade" tag="ul" :css="false" @before-enter="beforeEnter" @enter="enter" @leave="leave" > <li v-for="(item, index) in computedList" :key="item.msg" :data-index="index" > {{ item.msg }} </li> </transition-group> </div>
const Demo = { data() { return { query: '', list: [ { msg: 'Bruce Lee' }, { msg: 'Jackie Chan' }, { msg: 'Chuck Norris' }, { msg: 'Jet Li' }, { msg: 'Kung Fury' } ] } }, computed: { computedList() { var vm = this return this.list.filter(item => { return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1 }) } }, methods: { beforeEnter(el) { el.style.opacity = 0 el.style.height = 0 }, enter(el, done) { gsap.to(el, { opacity: 1, height: '1.6em', delay: el.dataset.index * 0.15, onComplete: done }) }, leave(el, done) { gsap.to(el, { opacity: 0, height: 0, delay: el.dataset.index * 0.15, onComplete: done }) } } } Vue.createApp(Demo).mount('#demo')
过渡可以通过 Vue 的组件系统实现复用。
要创建一个可复用过渡组件,你需要做的就是将 <transition>
或者 <transition-group>
作为根组件,然后将任何子组件放置在其中就可以了。
一个简单例子:
Vue.component('my-special-transition', { template: '\ <transition\ name="very-special-transition"\ mode="out-in"\ @before-enter="beforeEnter"\ @after-enter="afterEnter"\ >\ <slot></slot>\ </transition>\ ', methods: { beforeEnter(el) { // ... }, afterEnter(el) { // ... } } })
函数式组件更适合完成这个任务:
Vue.component('my-special-transition', { functional: true, render: function(createElement, context) { var data = { props: { name: 'very-special-transition', mode: 'out-in' }, on: { beforeEnter(el) { // ... }, afterEnter(el) { // ... } } } return createElement('transition', data, context.children) } })
在 Vue 中即使是过渡也是数据驱动的!
动态过渡是通过 name
属性来绑定动态的值。
<transition :name="transitionName"> <!-- ... --> </transition>
上面的transitionName是一个JS变量或者函数。
既然是数据驱动的,那么动态的不仅可以是变量,也可以是钩子方法:
<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" :max="maxFadeDuration" /> Fade Out: <input type="range" v-model="fadeOutDuration" min="0" :max="maxFadeDuration" /> <transition :css="false" @before-enter="beforeEnter" @enter="enter" @leave="leave" > <p v-if="show">hello</p> </transition> <button v-if="stop" @click="stop = false; show = false"> Start animating </button> <button v-else @click="stop = true">Stop it!</button> </div>
const app = Vue.createApp({ data() { return { show: true, fadeInDuration: 1000, fadeOutDuration: 1000, maxFadeDuration: 1500, stop: true } }, mounted() { this.show = false }, methods: { beforeEnter(el) { el.style.opacity = 0 }, enter(el, done) { var vm = this Velocity( el, { opacity: 1 }, { duration: this.fadeInDuration, complete: function() { done() if (!vm.stop) vm.show = false } } ) }, leave(el, done) { var vm = this Velocity( el, { opacity: 0 }, { duration: this.fadeOutDuration, complete: function() { done() vm.show = true } } ) } } }) app.mount('#dynamic-fade-demo')