过渡效果

概述

当从 DOM 中插入、更新或移除项目时,Vue 提供多种应用过渡效果的方式。包括以下工具:

本页面中,我们只会涉及到进入、离开和列表的过渡,然而你也可以查看下一章节管理过渡状态.

单元素/组件的过渡

Vue 提供了 transition 外层包裹容器组件(wrapper component),可以给下列情形中的任何元素和组件添加进入/离开(enter/leave)过渡

这是一个常见行为的简单示例:

<div id="demo">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade">
<p v-if="show">hello</p>
</transition>
</div>
new Vue({
el: '#demo',
data: {
show: true
}
})
.fade-enter-active, .fade-leave-active {
transition: opacity .5s
}
.fade-enter, .fade-leave-to /* .fade-leave-active 在低于版本 2.1.8 中 */ {
opacity: 0
}

hello

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

  1. 自动嗅探目标元素是否使用了 CSS 过渡或动画,如果使用,会在合适的时机添加/移除 CSS 过渡 class。

  2. 如果过渡组件设置了 JavaScript 钩子函数,这些钩子函数将在合适的时机调用。

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

过渡类名(Transition Classes)

有 6 种 class 类名会在进入/离开(enter/leave)过渡中处理

  1. v-enter:进入式过渡(entering transition)的开始状态。在插入元素之前添加,在插入元素之后一帧移除。

  2. v-enter-active:进入式过渡的激活状态。应用于整个进入式过渡时期。在插入元素之前添加,过渡/动画(transition/animation)完成之后移除。此 class 可用于定义进入式过渡的 duration, delay 和 easing 曲线。

  3. v-enter-to仅适用于版本 2.1.8+。进入式过渡的结束状态。在插入元素之后一帧添加(同时,移除 v-enter),在过渡/动画完成之后移除。

  4. v-leave:离开式过渡(leaving transition)的开始状态。在触发离开式过渡时立即添加,在一帧之后移除。

  5. v-leave-active:离开式过渡的激活状态。应用于整个离开式过渡时期。在触发离开式过渡时立即添加,在过渡/动画(transition/animation)完成之后移除。此 class 可用于定义离开式过渡的 duration, delay 和 easing 曲线。

  6. v-leave-to仅适用于版本 2.1.8+。离开式过渡的结束状态。在触发离开式过渡之后一帧添加(同时,移除 v-leave),在过渡/动画完成之后移除。

Transition Diagram

对于这些过渡中切换 class,每个都以过渡的 name 作为前缀。当您使用没有 name 的 <transition> 元素时,会默认前缀为 v-。举个例子,如果你使用 <transition name="my-transition">,那么默认的 v-enter class 将会被替换为 my-transition-enter

v-enter-activev-leave-active 可以指定不同的进入/离开过渡 easing 曲线,下面章节可以看到一个示例。

CSS 过渡(CSS Transitions)

最常用到的过渡类型是使用 CSS 过渡。下面是一个简单示例:

<div id="example-1">
<button @click="show = !show">
Toggle render
</button>
<transition name="slide-fade">
<p v-if="show">hello</p>
</transition>
</div>
new Vue({
el: '#example-1',
data: {
show: true
}
})
/* 进入和离开动画可以分别 */
/* 设置不同的持续时间(duration)和动画函数(timing function) */
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active 在低于 2.1.8 版本中 */ {
transform: translateX(10px);
opacity: 0;
}

hello

CSS 动画(CSS Animations)

CSS 动画用法和 CSS 过渡相同,区别是在动画中 v-enter 类名在元素插入 DOM 后不会立即删除,而是在 animationend 事件触发时删除。

这里是一个示例,为了简洁省略了 CSS 规则的前缀:

<div id="example-2">
<button @click="show = !show">Toggle show</button>
<transition name="bounce">
<p v-if="show">Look at me!</p>
</transition>
</div>
new Vue({
el: '#example-2',
data: {
show: true
}
})
.bounce-enter-active {
animation: bounce-in .5s;
}
.bounce-leave-active {
animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}

Look at me!

自定义过渡的 class 类名(Custom Transition Classes)

你也可以通过提供一下属性来指定自定义过渡类名

它们将覆盖默认约定的类名,这对于将 Vue 的过渡系统和其他现有的第三方 CSS 动画库(如 Animate.css)集成使用会非常有用。

这里是一个示例:

<link href="https://unpkg.com/animate.css@3.5.1/animate.min.css" rel="stylesheet" type="text/css">
<div id="example-3">
<button @click="show = !show">
Toggle render
</button>
<transition
name="custom-classes-transition"
enter-active-class="animated tada"
leave-active-class="animated bounceOutRight"
>
<p v-if="show">hello</p>
</transition>
</div>
new Vue({
el: '#example-3',
data: {
show: true
}
})

hello

同时使用过渡和动画(Using Transitions and Animations Together)

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

但是,在一些情况下,你可能需要给同一个元素同时设置过渡和动画,比如由 Vue 触发 CSS 动画,同时在鼠标悬停时触发 CSS 过渡。在这种情况下,你可能需要通过 type 属性,来显式声明需要 Vue 监听的类型,值可以是 animationtransition

显式过渡持续时间(Explicit Transition Durations)

2.2.0+ 新增

在大多数情况下,Vue 可以自动推断出过渡完成时间。默认情况下,Vue 会过渡根元素的第一个 transitionendanimationend 事件触发所需的等待时间。然而,这可能并不总是我们想要的 - 例如,我们可能具有设计安排的过渡序列(transition sequence):其中一些嵌套的内部元素(在根元素过渡完成后)还具有延续的过渡效果,或比过渡根元素更长的过渡持续时间。

在这种情况下,您可以使用 <transition> 组件上的 duration 属性 ,来指定一个显式的过渡持续时间(以毫秒为单位):

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

您还可以为进入式和离开式持续时间指定不同的值:

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

JavaScript 钩子函数

可以在属性中声明 JavaScript 钩子

<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:after-enter="afterEnter"
v-on:enter-cancelled="enterCancelled"
v-on:before-leave="beforeLeave"
v-on:leave="leave"
v-on:after-leave="afterLeave"
v-on:leave-cancelled="leaveCancelled"
>
<!-- ... -->
</transition>
// ...
methods: {
// --------
// 进入时
// --------
beforeEnter: function (el) {
// ...
},
// 在与 CSS 结合使用时
// 此回调函数 done 是可选项
enter: function (el, done) {
// ...
done()
},
afterEnter: function (el) {
// ...
},
enterCancelled: function (el) {
// ...
},
// --------
// 离开时
// --------
beforeLeave: function (el) {
// ...
},
// 在与 CSS 结合使用时
// 此回调函数 done 是可选项
leave: function (el, done) {
// ...
done()
},
afterLeave: function (el) {
// ...
},
// leaveCancelled 只能配合 v-show 使用
leaveCancelled: function (el) {
// ...
}
}

这些钩子函数可以结合 CSS 过渡/动画使用,也可以单独使用。

当仅使用 JavaScript 式过渡的时候, enterleave 钩子函数中,必须有 done 回调函数。否则,这两个钩子函数会被同步调用,过渡会立即完成。

推荐对于仅使用 JavaScript 的过渡显式添加 v-bind:css="false",以便 Vue 可以跳过 CSS 侦测。这也可以防止 CSS 规则意外干涉到过渡。

现在我们深入来看一个示例。这里是一个简单的使用 Velocity.js 的 JavaScript 式过渡:

<!--
Velocity works very much like jQuery.animate and is
a great option for JavaScript animations
-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<div id="example-4">
<button @click="show = !show">
Toggle
</button>
<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:leave="leave"
v-bind:css="false"
>
<p v-if="show">
Demo
</p>
</transition>
</div>
new Vue({
el: '#example-4',
data: {
show: false
},
methods: {
beforeEnter: function (el) {
el.style.opacity = 0
},
enter: function (el, done) {
Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 })
Velocity(el, { fontSize: '1em' }, { complete: done })
},
leave: function (el, done) {
Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 600 })
Velocity(el, { rotateZ: '100deg' }, { loop: 2 })
Velocity(el, {
rotateZ: '45deg',
translateY: '30px',
translateX: '30px',
opacity: 0
}, { complete: done })
}
}
})

Demo

在初始渲染时过渡

如果你还想在节点初始渲染时应用过渡,可以添加 appear 属性:

<transition appear>
<!-- ... -->
</transition>

默认情况下,对于进入和离开,会使用特定过渡。但是,如果你有需要,也可以指定自定义 CSS 类名:

<transition
appear
appear-class="custom-appear-class"
appear-to-class="custom-appear-to-class" (2.1.8+)
appear-to-class="custom-appear-to-class"(仅 >= 2.1.8 支持)
appear-active-class="custom-appear-active-class"
>
<!-- ... -->
</transition>

以及指定自定义 JavaScript 钩子函数:

<transition
appear
v-on:before-appear="customBeforeAppearHook"
v-on:appear="customAppearHook"
v-on:after-appear="customAfterAppearHook"
v-on:appear-cancelled="customAppearCancelledHook"
>
<!-- ... -->
</transition>

多个元素之间切换过渡

我们将在下面讨论多个元素之间切换过渡,但是还是可以使用 v-if/v-else,来对初始元素之间进行切换过渡。最常见的是,一个列表容器和描述列表为空的消息,这两个元素间的切换过渡:

<transition>
<table v-if="items.length > 0">
<!-- ... -->
</table>
<p v-else>Sorry, no items found.</p>
</transition>

可以这样使用,但是有一点事项需要注意:

当在具有相同标签名称的元素之间切换时,需要通过给它们分配唯一的 key 属性,以使 Vue 感知它们是不同的元素。否则 Vue 的编译器将因为效率,只会替换元素内部的内容。即使在技术上没有必要,但是,<transition> 组件中的多个元素设置 key,被认为是一个最佳实践。

示例:

<transition>
<button v-if="isEditing" key="save">
Save
</button>
<button v-else key="edit">
Edit
</button>
</transition>

在上面这种场景中,也通过给同一元素的 key 属性,设置不同的状态来进行过渡。而无需使用 v-ifv-else,所以上面的示例可以重写为:

<transition>
<button v-bind:key="isEditing">
{{ isEditing ? 'Save' : 'Edit' }}
</button>
</transition>

实际上,使用 v-if 的多个元素之间的过渡,还可以改为在单个元素上绑定动态属性的方式,来在任意数量的元素之间进行转换。例如:

<transition>
<button v-if="docState === 'saved'" key="saved">
Edit
</button>
<button v-if="docState === 'edited'" key="edited">
Save
</button>
<button v-if="docState === 'editing'" key="editing">
Cancel
</button>
</transition>

可以重写为:

<transition>
<button v-bind:key="docState">
{{ buttonMessage }}
</button>
</transition>
// ...
computed: {
buttonMessage: function () {
switch (this.docState) {
case 'saved': return 'Edit'
case 'edited': return 'Save'
case 'editing': return 'Cancel'
}
}
}

过渡模式

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

在 “on” 按钮和 “off” 按钮之间过渡时,这两个按钮会同步渲染 - 当一个过渡进入时,另一个过渡离开。这是 <transition> 的默认行为 - 进入和离开同时发生。

有时这样做是非常合理的,比如,在过渡的条目都是绝对定位时:

然后,还可以将元素位移,使它们看起来具有滑动过渡效果:

同时生效的进入式和离开式过渡不能满足所有要求,所以 Vue 提供了可选的过渡模式

现在,让我们用 out-in 模式,更新下前面的 on/off 按钮过渡:

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

只需添加一个额外的属性,就解决了最初的过渡问题,而无需添加任何特殊样式。

in-out 模式不是经常用到,但对于一些稍微不同的过渡效果还是有用的。尝试将这种模式与我们之前滑动淡出过渡的示例相结合:

很酷吧?

多个组件之间过渡

多个组件之间的过渡甚至更简单 - 我们不需要使用 key 属性。相反,我们只需要使用动态组件:

<transition name="component-fade" mode="out-in">
<component v-bind:is="view"></component>
</transition>
new Vue({
el: '#transition-components-demo',
data: {
view: 'v-a'
},
components: {
'v-a': {
template: '<div>Component A</div>'
},
'v-b': {
template: '<div>Component B</div>'
}
}
})
.component-fade-enter-active, .component-fade-leave-active {
transition: opacity .3s ease;
}
.component-fade-enter, .component-fade-leave-to
/* .component-fade-leave-active 在低于 2.1.8 版本中 */ {
opacity: 0;
}

列表过渡

目前为止,关于过渡我们已经完成了:

那么,当我们整个列表的每一项(例如使用 v-for)都需要同时进行渲染呢?在这种情况下,我们将使用 <transition-group> 组件。在我们深入示例之前,先来了解关于这个组件的一些要点:

进入式/离开式列表过渡

现在让我们来深入一个简单的示例,进入式过渡和离开式过渡都使用与之前相同的 CSS 类名:

<div id="list-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>
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)
},
}
})
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to /* .list-leave-active 在低于 2.1.8 版本中 */ {
opacity: 0;
transform: translateY(30px);
}
{{ item }}

这个示例有个问题,当添加和移除项目时,其周围的项目会瞬间猛地移动到新的位置,而不是平滑过渡,我们稍后会解决这个问题。

列表的位移过渡

<transition-group> 组件还有一个暗藏玄机之处。不仅可以在进入和离开时进行动画,还可以在位置改变时进行动画。使用此功能所需要知道的唯一新的概念是,在项目位置改变时添加额外的 v-move 类名。与其他类名相同,它的前缀也和设置的 name 属性的值相匹配,你也可以通过 move-class 属性来手动指定类名。

此类名对于指定过渡时间和 easing 过渡曲线非常有用,如下所示:

<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>
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)
}
}
})
.flip-list-move {
transition: transform 1s;
}
  • {{ item }}
  • 这个看起来很神奇,内部的实现原理是,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 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>
    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)
    }
    }
    })
    .list-complete-item {
    transition: all 1s;
    display: inline-block;
    margin-right: 10px;
    }
    .list-complete-enter, .list-complete-leave-to
    /* .list-complete-leave-active 在低于 2.1.8 版本中 */ {
    opacity: 0;
    transform: translateY(30px);
    }
    .list-complete-leave-active {
    position: absolute;
    }
    {{ item }}

    需要注意的是,使用 FLIP 过渡的元素,在设置为 display: inline 时,无法正常运行。作为替代方案,可以将元素设置为 display: inline-block,或者将元素放置于 flex 上下文(flex context)中。

    FLIP 动画不局限于单个轴线方向(single axis),多个维度网格(multidimensional grid)的过渡也同样简单

    Lazy Sudoku

    Keep hitting the shuffle button until you win.

    {{ cell.number }}

    列表的渐进过渡

    通过 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>
    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)
    }
    }
    })
  • {{ item.msg }}
  • 可复用的过渡

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

    这里是使用 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 属性(attribute)和动态属性(dynamic property)绑定在一起。

    <transition v-bind:name="transitionName">
    <!-- ... -->
    </transition>

    当你使用多个 Vue 过渡类名约定,来定义 CSS 过渡/动画,并在不同的类名约定之间切换时,动态过渡会非常有用。

    所有的过渡属性都可以动态绑定。并且不仅是属性,由于事件钩子函数都是 Vue 的方法(methods),所以可以从 this 上下文访问到所有数据。这意味着,根据组件的状态,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>
    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
    }
    }
    )
    }
    }
    })
    Fade In: Fade Out:

    hello

    最后,创建动态过渡的最终方案是,组件通过接受 prop 来动态修改之前的过渡。还是那句话,唯一限制你的是想象力。


    原文:https://vuejs.org/v2/guide/transitions.html