精读Vue官方文档之过渡 & 动画 (1)

概述

Vue 会基于元素的状态变更(增、删、改)来应用过渡效果。
Vue 内置的过渡系统既可以支持纯粹的CSS,JavaScript 实现过渡,也可以结合第三方 CSS动画库 (Animation.css) 以及 JavaScript动画库 (Velocity.js) 来实现更丰富过渡效果。

有哪些方面会触发元素或 DOM 的状态变更?

  • v-if 条件判断
  • v-show 条件展示
  • key 唯一标识的改变
  • component 动态组件匹配
  • vue-router 路由命中

Vue 内置的过渡系统

指提供了 <transition><transition-group> 组件。前者用于单元素/组件的过渡,而后者主要用于列表的过渡

单元素/组件的过渡

纯粹的CSS过渡

<transition> 组件会基于内部元素的状态变更,在恰当的时机有顺序的添加/删除以下CSS类名:

  • v-enter : 进入过渡的开始状态,在元素插入之前被添加,在插入元素后的下一帧被移除。
  • v-enter-active : 进入过渡的的生效状态,在元素插入之前被添加,在进入过渡的变化/动画完成后移除,在整个进入过渡阶段一直生效。这个类可以用于定义进入过渡的过程时间、曲线函数、延迟等。
  • v-enter-to : 进入过渡的完成状态,在元素插入的下一帧生效(同时 v-enter 被移除),在进入过渡的变化/动画完成后被移除(与 v-enter-active 一同移除)。
  • v-leave : 离开过渡的开始状态,在离开状态被触发时立刻被添加,下一帧被移除。
  • v-leave-active :离开过渡的生效状态,在离开过渡被触发时被添加,在离开过渡的变化/动画完成后移除,在整个离开过渡阶段一直生效。这个类可以用于定义进入离开过渡的过程时间、曲线函数、延迟等。
  • v-leave-to : 离开过渡的结束状态,在触发离开过渡的下一帧被添加(同时 v-leave 被移除),在离开过渡的变化/动画完成后移除(与 v-leave-active 一同移除).

以上6个CSS类名,3位一组,便可以构成整个进入/离开过渡的全部环节。然后在不同的时机动态切换这些 class 从而实现基于 CSS的过渡&动画。

可以通过下面的伪代码帮助我们更形象的理解这一过程:

进入过渡:

var oDiv = document.createElement('div');
oDiv.classList.add('v-enter');
oDiv.classList.add('v-enter-active');
document.body.appendChild(oDiv);
//下一帧
window.requestAnimationFrame(() => {
	oDiv.classList.remove('v-enter');
	oDiv.classList.add('v-enter-to');
});

//监听过渡完成
oDiv.addEventListener('transitionend',()=>{
	oDiv.classList.remove('v-enter-active');
	oDiv.classList.remove('v-enter-to');
})
复制代码

[v-enter & v-enter-active] → v-enter-active → [v-enter-active & v-enter-to]

离开过渡:


oDiv.classList.add('v-leave');
oDiv.classList.add('v-leave-active');

//下一帧
window.requestAnimationFrame(() => {
	oDiv.classList.remove('v-leave');
	oDiv.classList.add('v-leave-to');
});

//监听过渡完成
oDiv.addEventListener('transitionend',()=>{
	oDiv.classList.remove('v-leave-active');
	oDiv.classList.remove('v-leave-to');
})
复制代码

[v-leave & v-leave-active] → v-leave-active → [v-leave-active & v-leave-to]

<transition> 组件默认以 v-* 作为CSS钩子的命名前缀,我们可以通过 name attribute 进行自定义命名。

下面是一个完整的采用CSS实现的过渡变化示例:

<template>
    <div>
        <button @click="show=!show">click</button>
        <transition name="slide-fade">
            <p v-if="show">Slide Fade Content</p>
        </transition>
    </div>
</template>
<script>
    export default {
        data() {
            return {
                show: true
            }
        }
    }
</script>
<style>
    .slide-fade-leave-active, .slide-fade-enter-active {
        transition: all 1s;
    }
    /*进入时透明,并且平移30px,应用过渡,然后过渡到不透明,不平移状态(由 v-enter-to 缺省)*/
    /*离开时,由不透明,不平移(由 v-leave-to 缺省) 过渡到透明且平移30px状态*/
    .slide-fade-enter, .slide-fade-leave-to {
        opacity: 0;
        transform: translateX(30px);
    }
</style>
复制代码

下面则是一个完整的采用CSS实现的过渡动画的示例,其实很简单的,因为 *-enter-active*-leave-active 在整个进入/离开过渡阶段都有效,所以只需要在这两个 class 中编写动画样式即可。

<style>
    @keyframes slide-fade {
        from{
            transform:translateX(30px);
            opacity:0
        }
        to{
            transform:translateX(0px);
            opacity:1
        }
    }
   .slide-fade-enter-active{
       animation:slide-fade 1s;
   }
    .slide-fade-leave-active{
        animation:slide-fade 1s reverse
    }

</style>
复制代码

结合 Animation.css 动画库

<transition> 组件支持重命名有CSS钩子的名称,好处就是可以无缝的引入像 Animation.css 此类动画库已经预定义好的CSS类。

<template>
    <div>
        <link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css">
        <button @click="show=!show">click</button>
        <transition enter-active-class="animated flipInY" leave-active-class="animated flipOutY">
            <p v-if="show">Slide Fade Content</p>
        </transition>
    </div>
</template>
<script>
    export default {
        data() {
            return {
                show: true
            }
        }
    }
</script>
复制代码
  • enter-class : 定义进入开始阶段的类名。
  • enter-active-class : 定义进入生效阶段的类名。
  • enter-to-class : 定义进入结束阶段的类型。
  • leave-class : 定义离开开始阶段的类名。
  • leave-active-class : 定义离开生效阶段的类名。
  • leave-to-class : 定义离开结束阶段的类名。

同时使用过渡与动画

<transition> 组件支持同时使用 transitionanimation 来实现过渡效果。但受限于两者可能的过渡时长不同,过渡时间存在先后问题,因而我们必须要通过 type Attribute 告诉 <transition> 组件应该以哪种过渡方式作为过渡完成的依据。

<transition name="fade" type="animation">
//....
</transition>
复制代码

指定持续时间

<transition> 组件默认监听根元素上的 transitionendanimationend 来判定是否完成过渡,这在大多数的场景都适用,但是对于一些经过精心安排的过渡系列(可以理解为多个过渡组成的队列)或者存在过渡效果的内部嵌套元素,对于这类情况再监听根元素就无法准确的判断过渡的延迟或持续时间。

好在,<transition> 组件提供了一个 duration Attribute,让开发者可以手动指定过渡的总时长,单位毫秒。

<transition :duration="1000">...</transition>
复制代码

你也可以分别定制进入和移出的持续时间:

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

纯粹的JS过渡

与CSS过渡类似 <transition> 组件为进入/离开过渡的每个阶段都提供了对应的钩子函数。

  • beforeEnter(el) : 进入之前
  • enter(el, done) : 进入中
  • afterEnter(el) : 进入之后
  • enterCancelled(el) : 进入取消
  • beforeLeave(el) : 离开之前
  • leave(el, done) : 离开中
  • afterLeave(el) : 离开后
  • leaveCancelled(el) : 离开取消

最常用的钩子函数主要还是 beforeEnter(el)enter(el, done)leave(el, done) 三个,需要注意 enterleave 中的回调函数 done,如果回调函数没有执行,它们将被同步调用,过渡会立即完成。

另外,我们可以使用 v-bind:css 属性关闭CSS检测,以免在过渡的过程中受到样式的意外影响。

<transition :css="false">
复制代码

下面是一个完整的基于纯粹的 JavaScript 实现的过渡示例:

<template>
    <div>
        <button @click="show=!show">click</button>
        <transition :css="false" @enter="enter" @before-enter="beforeEnter" @leave="leave" @before-leave="beforeLeave">
            <p v-if="show">Slide Fade Content</p>
        </transition>
    </div>
</template>
<script>
    export default {
        data() {
            return {
                show: true
            }
        },
        methods: {
            fade(time, el, cb) {
                window.requestAnimationFrame(() => {
                    const opacity = parseFloat(el.style.opacity);
                    if (opacity >= 1 && time > 0) return cb();
                    if (opacity <= 0 && time < 0) return cb();
                    el.style.opacity = opacity + time;
                    this.fade(time, el, cb);
                })
            },
            beforeEnter(el) {
                el.style.opacity = 0;
            },
            enter(el, done) {
                this.fade(0.05, el, done);
            },
            beforeLeave(el){
                el.style.opacity = 1;
            },
            leave(el, done) {
                this.fade(-0.05, el, done);
            }
        }
    }
</script>
复制代码

结合 Velocity.js 动画库

利用JS钩子函数结合 Velocity.js 动画库实现过渡效果。

new Vue({
  el: '#example-4',
  data: {
    show: false
  },
  methods: {
    beforeEnter: function (el) {
      el.style.opacity = 0
      el.style.transformOrigin = 'left'
    },
    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 })
    }
  }
});
复制代码

初始渲染的过渡

<transition> 组件的 appear Attribute 可以为初始渲染的元素或组件赋予过渡效果。其实很简单,一旦设置了 appear 属性,<transition> 组件默认会立即调用一次进入阶段的过渡效果。

到了这里我们可以总结下,Vue内置的过渡系统提供了 出现 → 进入 → 离开 三个阶段的过渡效果。

<transition> 组件为出现阶段也提供了类似进入/离开的 CSS 钩子与 JavaScript 钩子函数,利用这些钩子我们可以自定义出现的过渡效果。

出现过渡提供的CSS类名

  • appear-class : 出现过渡的开始状态。
  • appear-active-class : 出现过渡的生效状态。
  • appear-to-class : 出现过渡的结束状态。

**出现过渡提供的 JavaScript钩子 **

  • before-appear
  • appear
  • after-appear
  • appear-cancelled

多元素过渡

这里元素专指原始HTML标签,关于多组件过渡,后面会说到。

<transition> 组件虽然同一时刻只能对一个元素进行过渡,但是过渡本身又可以分为进入过渡离开过渡两部分。所以,多元素过渡指的是进入过渡与离开过渡的对象可以不是同一元素(不同的标签)。

实现多元素过渡非常简单,直接在 <transition> 组件中使用 v-if/v-else 即可做到,下面是一个具体的实例:

<template>
    <div>
        <button @click="notContent=!notContent">{{notContent ? 'There is not Data' : 'There is Data'}}</button>
        <transition>
            <p v-if="notContent">Not Content - Empty</p>
            <table v-else align="center">
                <tr>
                    <td>11111</td>
                </tr>
                <tr>
                    <td>22222</td>
                </tr>
            </table>
        </transition>
    </div>
</template>
<script>
    export default {
        data() {
            return {
                notContent: false
            }
        }
    }
</script>
<style>
    .v-enter, .v-leave-to {
        opacity: 0
    }
    .v-enter-active, .v-leave-active {
        transition: opacity .5s;
    }

</style>
复制代码

如果进入过渡离开过渡的元素具有相同的标签名,那么元素切换时需要通过 key attribute 进行唯一标记,以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。

<transition>
    <table align="center" border="1" :key="notContent">
        <tr>
            <td>Not Content!</td>
        </tr>
    </table>
    <table align="center" border="1" :key="notContent">
        <tr>
            <td>11111</td>
        </tr>
        <tr>
            <td>22222</td>
        </tr>
    </table>
</transition>
复制代码

由于为元素赋予 key Attribute 已经可以帮助 <transition> 组件锚定进入/离开的目标元素,所以此时还可以省去 v-if/v-else 的写法。

最后,多元素过渡还会涉及到过渡的顺序问题,对此 <transition> 组件还提供了 mode Attribute,用来控制进入、离开的过渡顺序。

<!--先出后进 | 先进后出-->
<transition mode="out-in | in-out"></transtion>
复制代码

多组件过渡

多组件过渡非常简单,对比多元素过渡我们不需要使用 v-if/v-else 也不需要设置 key,直接使用动态组件技术即可。

列表过渡

使用全新的 <transition-group> 组件来对列表进行过渡,对比 <transition> 组件主要有以下区别:

TransitionGroup Transition
可以为多个元素同一时刻应用进入过渡或离开过渡 单个节点的进入离开;同一时间为多个节点中的一个应用进入离开。
会以一个真实的元素去呈现,默认为 <span>。可以通过 tag attribute 更换为其他元素 使用空白元素
不支持过渡模式 mode。因为不需要限制进入/离开的交替行为 可通过 mode attribute 限制进入或离开的过渡顺序
因为是列表项存在多个元素,所以必须要指定 key 只有相同名称的多元素过渡才需要指定 key

Vue 内部使用了一个叫 Flip 的简单动画队列插件来实现列表的过渡,其原理也非常简单:

获取元素第一帧时的各个属性状态,然后将元素直接置为最后一帧的状态同样保存相关的属性数据,最后基于起始和结束两个状态生成补间状态,结合使用 transform 将元素平滑的从之前的位置过渡到新的位置。

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

使用CSS实现列表过渡

与单个组件/元素的 CSS 过渡相同,主要差别是增加了新的 v-move CSS钩子,可以在列表项改变定位时应用,也可以通过 move-class 自定义 v-move 的名称,以便与第三方 CSS 动画库结合使用。

<template>
    <div>
        <button @click="add">add</button>
        <button @click="remove">remove</button>
        <hr/>
        <transition-group tag="p">
            <span class="list-item" v-for="item in items" :key="item">{{item}}</span>
        </transition-group>
    </div>
</template>
<script>
    export default {
        data() {
            return {
                items: [1, 2, 3, 4, 5, 6],
                nextItem: 7
            }
        },
        methods: {
            add() {
                const randomIndex = ~~(Math.random() * this.items.length);
                this.items.splice(randomIndex, 0, this.nextItem++);
            },
            remove() {
                const randomIndex = ~~(Math.random() * this.items.length);
                this.items.splice(randomIndex, 1);
            }
        }
    }
</script>
<style>
    .list-item {
        display: inline-block;
        padding: 5px;
    }

    .v-enter, .v-leave-to {
        opacity: 0;
        transform: translateY(30px);
    }

    .v-enter-active, v-leave-active {
        transition: all .5s;
    }
</style>
复制代码

点击插入元素,可以发现插入的元素具有过渡效果,但是插入位置相邻的元素并没有过渡效果,这是因为没有开启列表项元素在发生定位时应用过渡的功能,现在加入下面的 CSS :

.v-move {
    transition: all .5s;
}
复制代码

可以发现插入时过渡效果都正常了,但是点击删除时,相邻的元素又没有过渡效果了并且是瞬间完成,这是受 transfrom 自身特性导致的,transform 在平移元素时,并不会改变元素所在文档流中占据的位置空间,从而导致 Flip 动画队列没能检测到列表项元素定位发生改变,便没有应用 v-move

image.png

既然知道了原理,那么解决起来就非常简单,我们只需要在触发离开过渡时将其从文档流中移出即可。

.list-item {
    display: inline-block;
    padding: 5px;
}

.v-enter, .v-leave-to {
    opacity: 0;
    transform: translateY(30px);
}

.v-enter-active, v-leave-active {
    transition: all .5s;
}
.v-move {
    transition: all .5s;
}
.v-leave-active {
    position: absolute;
}
复制代码

实际上,还可以再进一步简化代码,因为 .v-move.v-enter-active,.v-leave-active 中的 CSS声明对于列表项元素而言一直都是产生作用的,所以我们可以将它们合并到 .list-item 类中。

.list-item {
    display: inline-block;
    padding: 5px;
    transition: all .5s;
}

.v-enter, .v-leave-to {
    opacity: 0;
    transform: translateY(30px);
}

.v-leave-active {
    position: absolute;
}
复制代码

通过 JS 钩子实现列表的交错过渡

通过使用 data attribute 来记录元素的状态数据然后与 JavaScript 通信,就可以实现列表的交错过渡:

官方文档提供了一个结合 Velocity.js 的示例代码:列表的交错过渡

下面是通过JS钩子结合CSS动画类实现的简易交错动画示例:

<template>
    <div>
        <link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css">
        <transition-group tag="ul" @enter="enter" appear :css="false">
            <li class="list-item" v-for="item in 10" :key="item" :data-index="item">{{item}}</li>
        </transition-group>
    </div>
</template>
<script>
    export default {
        methods: {
            enter(el, done) {
                const delay = el.dataset.index * 200;
                setTimeout(() => {
                    el.classList.add('animated');
                    el.classList.add('tada');
                    el.style.opacity = 1;
                    setTimeout(() => done, 150)
                }, delay)
            }
        }
    }
</script>
<style>
    .list-item {
        border: 1px solid #eee;
        width: 200px;
        list-style: none;
        height: 35px;
        line-height: 30px;
        opacity: 0
    }
</style>
复制代码

可复用过渡

通过Vue的组件系统创建一个可复用的过渡组件,需要做的就是将过渡效果的配置与 <transition><transition-group> 封装在一个自定义组件中即可。
官方文档示例:把过渡放到组件里

动态过渡

通过数据驱动的方式来动态改变过渡方式。

  • 基于CSS钩子实现的过渡,可以通过切换 <transition><transition-group> 组件的 name 名称实现。
  • 基于JavaScript钩子实现的过渡,可以动态读取Vue的响应式变量参数的方式来实现。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享