目前为止,关于过渡我们已经讲到:
- 单个节点
- 多个节点,每次只渲染一个
那么怎么同时渲染整个列表,比如使用 v-for
?在这种场景下,我们会使用 <transition-group>
组件。在我们深入例子之前,先了解关于这个组件的几个特点:
- 默认情况下,它不会渲染一个包裹元素,但是你可以通过
tag
attribute 指定渲染一个元素。 - 过渡模式不可用,因为我们不再相互切换特有的元素。
- 内部元素总是需要提供唯一的
key
attribute 值。 - CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。
列表的进入/离开过渡
现在让我们由一个简单的例子深入,进入和离开的过渡使用之前一样的 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>
JS代码:
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')
CSS代码:
.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);
}
完整DEMO如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>列表过渡 | Web176教程(web176.com)</title> <style> .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); } </style> </head> <body> <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> <script src='https://unpkg.com/vue@next'></script> <script> 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') </script> </body> </html>
这个例子有一个问题,当添加和移除元素的时候,周围的元素会瞬间移动到它们的新布局的位置,而不是平滑的过渡,我们下面会解决这个问题。
列表的移动过渡
<transition-group>
组件还有一个特殊之处。除了进入和离开,它还可以为定位的改变添加动画。只需了解新增的 v-move
类就可以使用这个新功能,它会应用在元素改变定位的过程中。像之前的类名一样,它的前缀可以通过 name
attribute 来自定义,也可以通过 move-class
attribute 手动设置。
这个类主要用于指定过渡时长和缓动效果曲线,如下所示:
<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>
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;
}
完整DEMO如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>列表过渡 | Web176教程(web176.com)</title> <style> .flip-list-move { transition: transform 0.8s ease; } </style> </head> <body> <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> <script src='https://unpkg.com/vue@next'></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script> <script> 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') </script> </body> </html>
这个看起来很神奇,其实 Vue 内部使用了一个叫 FLIP 的动画技术,它使用 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;
}
完整DEMO如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>列表过渡 | Web176教程(web176.com)</title> <style> .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; } </style> </head> <body> <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> <script src='https://unpkg.com/vue@next'></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script> <script> 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') </script> </body> </html>
TIPs
需要注意的是使用 FLIP 过渡的元素不能设置为
display: inline
。作为替代方案,可以设置为display: inline-block
或者将元素放置于 flex 布局中。
列表的交错过渡
通过 data attribute 与 JavaScript 通信,就可以实现列表的交错过渡:
<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')
完整DEMO如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>列表过渡 | Web176教程(web176.com)</title> <style> .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; } </style> </head> <body> <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> <script src='https://unpkg.com/vue@next'></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js"></script> <script> 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') </script> </body> </html>
可复用的过渡
过渡可以通过 Vue 的组件系统实现复用。要创建一个可复用的过渡组件,你需要做的就是将 <transition>
或者 <transition-group>
作为根组件,然后将任何子组件放置其中就可以了。
TODO:使用 Vue3 重构
使用 template 的简单例子:
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
attribute 来绑定动态的值。
<transition :name="transitionName">
<!-- ... -->
</transition>
当你已经定义了 Vue 的过渡类约定,并希望可以快速切换它们的时候,这会非常有用。
尽管所有过渡 attribute 都可以动态绑定,但我们可用的不只有 attribute。因为事件钩子是方法,它们可以访问任何上下文中的数据,这意味着根据组件状态的不同,你的 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" :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')
看下完整实例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>动态过渡 | Web176教程(web176.com)</title> <style> .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; } </style> </head> <body> <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> <script src='https://unpkg.com/vue@next'></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script> <script> 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') </script> </body> </html>
最后,创建动态过渡的最终方案是组件通过赋值 prop 来动态修改之前的过渡。一句老话,唯一的限制是你的想象力。
作者:terry,如若转载,请注明出处:https://www.web176.com/vue3/6216.html