「 Vue 」在 Vue 中使用过渡和动画

1,062 阅读5分钟

这是我参与11月更文挑战的8天,活动详情查看:2021最后一次更文挑战

概述

在 Vue 中,我们可以通过多种方法实现过渡和动画效果,包括 class 和 style 以及 <transition> 标签实现单元素 / 组件及多元素 / 组件之间的过渡效果,通过 <transition-group> 标签实现列表过渡,通过 Vue 的响应性和组件系统实现状态过渡。

基于 class 的过渡和动画

/* 动画 */
@keyframes leftToRight {
    0% {
        transform: translateX(-100px);
    }

    50% {
        transform: translateX(-50px);
    }

    0% {
        transform: translateX(0px);
    }
}

.animation {
    animation: leftToRight 3s;
}

/* 过渡 */
.transition {
    transition: 3s background-color ease;
}

.yellow {
    background-color: yellow;
}

.skyblue {
    background-color: skyblue;
}
const app = Vue.createApp({
    data() {
        return {
            animation: {
                animation: true
            },
            transition: {
                transition: true,
                skyblue: true,
                yellow: false
            }
        }
    },
    methods: {
        handleClickAnimation() {
            this.animation.animation = !this.animation.animation
        },
        handleClickTransition() {
            this.transition.skyblue = !this.transition.skyblue;
            this.transition.yellow = !this.transition.yellow;
        }
    },
    template:/*html*/ `
    <div :class='animation'>Hello Animarion</div>
    <button @click='handleClickAnimation'>change</button>
    <div :class='transition'>Hello Transiton</div>
    <button @click='handleClickTransition'>change</button>
    `
})
const vm = app.mount("#root")

demo1.gif

通过 style 控制

const app = Vue.createApp({
    data() {
        return {
            styleObj: {
                background: 'skyblue'
            }
        }
    },
    methods: {
        handleClick() {
            this.styleObj.background === 'skyblue' ?
                this.styleObj.background = 'yellow' :
                this.styleObj.background = 'skyblue'
        }
    },
    template:/*html*/ `
    <div class='transition' :style='styleObj'>Hello Transiton</div>
    <button @click='handleClick'>change</button>
    `
})

进入 / 离开过渡

当插入、更新和移除 DOM 时, Vue 提供了多种应用转换效果的方法。

单元素 / 组件的过渡

CSS 过渡

通过 <transition> 标签控制一个元素即单元素的过渡。元素从展示态变为隐藏态即出场,元素从展示态变为隐藏态即入场。下面的例子通过 v-if 来控制元素的渲染,除此之外还可以使用 v-show 控制元素的展示:

const app = Vue.createApp({
    data() {
        return {
            show: false
        }
    },
    methods: {
        handleClick() {
            this.show = !this.show
        }
    },
    template:/*html*/`
    <transition>
        <div v-if='show'>Hello World</div>
    </transition>
    <button @click='handleClick'>change</button>
    `
})

之后我们需要添加一些样式。

过渡 class

v-enter-from是指元素入场的开始状态,在元素被插入之后的下一帧移除;v-enter-to是指入场的结束状态,在过渡/动画完成之后移除;v-enter-active定义进入生效时的状态,在过渡/动画完成之后移除:

.v-enter-from {
    opacity: 0;
}

.v-enter-active {
    transition: opacity 3s ease-out;
}

.v-enter-to {
    opacity: 1;
}

出场过程也有三个 class 切换,分别是v-leave-fromv-leave-activev-leave-to

Transition Diagram

上图解释了各个类的生命周期。

对于这些在过渡中切换的类名来说,如果你使用一个没有名字的 <transition>,则 v- 是这些 class 名的默认前缀。如果你使用了 <transition name="my-transition">,那么 v-enter-from会替换为 my-transition-enter-from

CSS 动画

CSS 动画的用法同 CSS 过渡,使用的也是 <transition> 标签。区别是在动画中 v-enter-from 类名在节点插入 DOM 后不会立即删除,而是在 animationend 事件触发时删除。

.v-enter-active {
  animation: bounce-in 0.5s;
}
.v-leave-active {
  animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
	/* ... */
}

自定义动画类名

通过以下属性可以自定义类名:enter-from-classenter-active-classenter-to-classleave-from-classleave-active-classleave-to-class

由此可以使用第三方动画库。

同时使用过渡和动画

在同时使用过渡和动画的情况下,<transition> 标签的 type attribute 可以设置以动画或过渡之一的时间为准,值可以是transitionanimation

.v-enter-active {
    transition: opacity 3s ease-out;
    animation: bounce-in 5s reverse;
}
<transition type='transiton'>...</transition>

上面的例子中,过渡和动画都在 3 秒后结束。

显性过渡持续时间

我们也可以通过duration设置一个显性的过渡持续时间,单位为毫秒,时间结束后过渡和动画效果都结束:

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

还可以定制进入和移出的持续时间:

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

JavaScript 钩子

我们可以不使用 CSS 过渡和动画,转而使用 JS 的过渡和动画效果。

使用:css='false'跳过 CSS 检测,通过 JavaScript 钩子来定义动画效果。

<transition
  @before-enter="beforeEnter"
  @enter="enter"
  @after-enter="afterEnter"
  @enter-cancelled="enterCancelled"
  @before-leave="beforeLeave"
  @leave="leave"
  @after-leave="afterLeave"
  @leave-cancelled="leaveCancelled"
  :css="false"
>
  <!-- ... -->
</transition>
methods: {
  beforeEnter(el) {
    el.style.color='red'
  },
  // 当与 CSS 结合使用时,回调函数 done 是可选的
  enter(el, done) {
    done() // 表示 enter 函数执行结束
  },
  // enter 执行结束之后才执行 afterEnter
  afterEnter(el) {
    // ...
  },
  enterCancelled(el) {
    // ...
  }
	// ...
}

初始渲染的过渡

可以通过 appear attribute 设置节点在初始渲染的过渡:

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

多个元素之间的过渡

我们可以通过v-ifv-elsev-else-if来实现元素之间的切换。默认情况下,<transition>元素的进入和离开是同时发生的,会导致同时出现两个元素。

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

  • in-out: 新元素先进行过渡,完成之后当前元素过渡离开。
  • out-in: 当前元素先进行过渡,完成之后新元素过渡进入。
<transition mode='out-in'>
	<div v-if='show'>Hello World</div>
	<div v-else>Bye World</div>
</transition>
<button @click='handleClick'>change</button>

多个组件之间过渡

在组件中,我们除了使用v-ifv-show之外,还可以使用动态组件来控制组件的切换。

列表过渡

目前我们只说了单元素、多元素中某一个元素的动画效果。现在介绍多元素的动画。

列表的进入 / 离开过渡

我们可以通过<transition-group>来渲染整个列表。

const app = Vue.createApp({
    data() {
        return {
            list: [1, 2, 3]
        }
    },
    methods: {
        handleClick() {
            this.list.unshift(this.list.length + 1)
        }
    },
    template:/*html*/ `
    <transition-group>
        <span class='list-item' v-for='item in list' :key='item'>{{item}}</span>
    </transition-group>
    <button @click='handleClick'>add</button>
    `
})
.list-item {
    display: inline-block;
    margin-right: 10px;
}

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

.v-enter-active {
    transition: all 2s ease;
}

这里的列表中的每一项需要提供一个唯一的key值,否则动画不生效。

此时有一个问题,列表的元素每增加一项,周围的元素会瞬间移动到他们所在的位置。

列表的移动过渡

为了解决刚刚提到的问题,我们可以使用v-move类来应用于元素改变的过程中。

.v-move {
    transition: all .5s ease-in
}

demo2.gif

状态过渡

当数据本身发生变化时,我们可以利用 Vue 的响应性和组件系统实现状态过渡的效果。

下面是一个例子:

const app = Vue.createApp({
    data() {
        return {
            number: 1,
            animateNumber: 1
        }
    },
    methods: {
        handleClick() {
            this.number = 10;
            if (this.animateNumber < this.number) {
                const animation = setInterval(() => {
                    this.animateNumber += 1
                    if (this.animateNumber === 10) {
                        clearInterval(animation)
                    }
                }, 100)
            }
        }
    },
    template:/*html*/ `
    <div>{{animateNumber}}</div>
    <button @click='handleClick'>add</button>
    `
})

点击按钮时,数字会从 1 逐一增加到 10,它是以动画的形式展示出来的。

demo3.gif