vue过渡与动画

1,489 阅读10分钟

一、Dom节点的状态

Dom节点在页面中只有两种状态,显示与不显示,因此,如果我们不考虑在这两种状态切换的过程加入一点优美的过渡效果,那么基本就是很直接的显示与不显示,下面看一下没有动画效果的切换是怎么实现的

<html>
	<head>
		<title>vue动画实现</title>
		<meta charset="utf-8"/>
		<script src="https://cdn.bootcss.com/vue/2.5.21/vue.js"></script>
	</head>
	<body>
	  <div id="app">
		<input type="button" value="点我切换" @click="flag=!flag" />
		<h3 v-if="flag">hello world</h3>
	  </div>
	  <script>
	  	var vm=new Vue({
	  		el:'#app',
	  		data:{
	  			flag:false
	  		},
	  		methods:{}
	  	})
	    
	  </script>
</body>
</html>

为了解决这个尴尬的问题,vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。

  • 在 CSS 过渡和动画中自动应用 class
  • 可以配合使用第三方 CSS 动画库,如 Animate.css
  • 在过渡钩子函数中使用 JavaScript 直接操作 DOM
  • 可以配合使用第三方 JavaScript 动画库,如 Velocity.js

在vue中设置动画效果时,要把需要添加的元素放在transition组件中,当插入或者删除包含transition组件中的元素时,vue将会做一下处理:

  1. 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。
  2. 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。
  3. 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。(注意:此指浏览器逐帧动画机制,和 Vue 的 nextTick 概念不同)

二、css过渡

在进入/离开的过渡中,会有 6 个 class 切换

  1. v-enter:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
  2. v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
  3. v-enter-to:2.1.8 版及以上定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter 被移除),在过渡/动画完成之后移除。
  4. v-leave:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
  5. v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
  6. v-leave-to:2.1.8 版及以上定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成之后移除。

用一张图来描述过渡效果从无到出现,从有到无类名的应用情况

可以看到,Dom元素从无到有这整个过程v-enter-active类名都是会存在的,但是这个过程一旦完成,该类名就会被移除,同样,Dom元素从有到无的整个过程,v-leave-active也是存在的,这个过程完成后,该类名也会被移除,因此我们可以利用这两个类名来实现监控dom元素从无到有,从有到无,这两个过程中某个样式的变化并赋予一定的完成时间,这样就可以实现过渡效果。

需要特别注意的是:

上面列举的类名最终都是会移除的,也就是说在这些类名中设置的位置,颜色,大小等设置过的样式也会跟着消失。我们要关注的点是,如何利用这些昙花一现的类名来设置一些我们想要的过渡效果,看一个很简单的例子

<template>
<div>
  <transition>
    <p v-if="show">hello</p>
  </transition>
  <button @click="show=!show">点击我试试</button>
</div>
</template>

<script>
  import Vue from 'vue'
  export default {
    components: {
    },
    data(){
      return {
        show:true
      }
    }
  }
</script>
<style>
.v-enter {
  font-size: 30px;
}
.v-enter-to {
  color: #FF0000;
}
.v-leave {
  transform: scale(2);
}
.v-leave-to {
  color: #5555ff;
}
.v-enter-active {
  transition: all 4s;
}
.v-leave-active {
  transition: all 4s;
}
</style>

我在vue中写下 这段代码,运行,你可以发现

transition组件的p元素的从无到有,即v-if的值从false→true的时候,v-enter,v-enter-to是会在合适的时机应用到元素上的,但是p元素的从有到无,即v-if的值从true→false的时候,v-leave却没有被应用,只有v-leave-to被应用了,我看了下我的vue版本是2.6.10完全没有问题

惊讶,transition组件中,当插入或者删除元素时的过渡效果的是实现逻辑不是一样的吗?为什么插入元素过程v-enter,v-enter-to会被应用,删除元素过程v-leave却没有被应用,只有v-leave-to被应用,我仔细的回头看了下官方文档对这几个类名的描述,发现,v-enter和v-leave都是在“下一帧”就被移除,“下一帧”?leave谁能解释一帧是多久?我在控制台并没有看到v-enter和v-leave类名被应用然后被移除这么一个过程,可见一帧的时间应该时很短的,但是v-enter的样式效果确实出现,因此可以知道v-enter是被应用了,v-leave的样式效果没有出现,v-leave应该没有被应用,到了这里,我看到很四年前有人曾经在github上问过整个问题,作者鱿鱼须是这么回答的

到这里,至于v-leave的类名不会被应用这个问题,我也得不出最准确的原因

只能说,在使用css过渡时,尽量少使用v-enter,v-leave这两个类名

三、css动画

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

使用css动画首先要定义好一个动画,然后在transition组件中插入,或则删除元素时在v-enter-active,或者v-leave-active类名中加载这个动画即可

<template>
<div>
  <transition>
    <p v-if="show">hello</p>
  </transition>
  <button @click="show=!show">点击我试试</button>
</div>
</template>


<script>
  import Vue from 'vue'
  export default {
    components: {
    },
    data(){
      return {
        show:true
      }
    }
  }
</script>
<style>
@keyframes bounce-in {
  0% {
    font-size: 10px;
  }
  50% {
    font-size: 20px;
  }
  100% {
    font-size: 30px;
  }
}
@keyframes bounce-out {
  0% {
    color: #0000FF;
  }
  50% {
    color: #FF0000;
  }
  100% {
    color: #F7B500;
  }
}
.v-enter-active {
  animation: bounce-in .5s;
}
.v-leave-active {
  animation: bounce-out .5s;
}

</style>

四、自定义类名

到了这里,需要注意的是,在使用css过渡和css动画时,可以使用自定义的类名

<html>
	<head>
		<title>vue动画实现</title>
		<meta charset="utf-8"/>
		<script src="https://cdn.bootcss.com/vue/2.5.21/vue.js"></script>
		<style type="text/css">
			/*这个相当默认的状态*/
			.v-enter,.v-leave-to{
				transform: translateX(150px);
			}
			/*这个过程用来设置过渡的时间,动画曲线等*/
			.v-enter-active,.v-leave-active{
				transition: all 0.8s ease;
			}
            
			/*自定义的要加上前缀来加以区分*/
		    .my-enter,
		    .my-leave-to {
		      transform: translateY(70px);
		    }
			/*自定义的要加上前缀来加以区分*/
		    .my-enter-active,
		    .my-leave-active{
		      transition: all 0.8s ease;
		    }
		</style>
	</head>
	<body>
	  <div id="app">
		<input type="button" value="点我切换" @click="flag=!flag" />
		<transition>
			<h3 v-if="flag">hello world</h3>
		</transition>
          
		<!--自定义的动画要把前缀加在name属性上-->
	    <transition name="my">
	      <h3 v-if="flag">这是我自己的动画</h3>
	    </transition>
	  </div>
	  <script>
	  	var vm=new Vue({
	  		el:'#app',
	  		data:{
	  			flag:false,
	  			flag2: false
	  		},
	  		methods:{}
	  	})
	  </script>
</body>
</html>

除了上面的方式,还可以通过以下 attribute 来自定义过渡类名:

  • enter-class
  • enter-active-class
  • enter-to-class (2.1.8+)
  • leave-class
  • leave-active-class
  • leave-to-class (2.1.8+)
<template>
<div>
  <transition
    enter-active-class="example-in"
    leave-active-class="example-out"
  >
    <p v-if="show">hello</p>
  </transition>
  <button @click="show=!show">点击我试试</button>
</div>
</template>


<script>
  import Vue from 'vue'
  export default {
    components: {
    },
    data(){
      return {
        show:true
      }
    }
  }
</script>
<style>
@keyframes bounce-in {
  0% {
    font-size: 10px;
  }
  50% {
    font-size: 20px;
  }
  100% {
    font-size: 30px;
  }
}
@keyframes bounce-out {
  0% {
    color: #0000FF;
  }
  50% {
    color: #FF0000;
  }
  100% {
    color: #F7B500;
  }
}
.example-in {vue
  animation: bounce-in .5s;
}
.example-out {
  animation: bounce-out .5s;
}
</style>

五、动画库animate.css的应用

animate.css官方文档

在引入animate.css的前提下,直接在HTML结构中使用

<h1 class="animate__animated animate__bounce">An animated element</h1>

而在vue中,我们已经学会了使用attribute来自定义类名,也就是说如果我们引入了动画库,自定义的类名指向动画库中的某一个类名,即加载了相应的动画

首先安装

npm install animate.css --save

在main.js中引入

import animate from 'animate.css'

<template>
<div>
  <transition
    enter-active-class="animate__animated animate__bounce animate__delay-2s"
    leave-active-class="example-out"
  >
    <h1 v-if="show">An animated element</h1>
  </transition>
  <button @click="show=!show">点击我试试</button>
</div>
</template>

<script>
  import Vue from 'vue'
  export default {
    components: {
    },
    data(){
      return {
        show:true
      }
    }
  }
</script>
<style>
@keyframes bounce-out {
  0% {
    color: #0000FF;
  }
  50% {
    color: #FF0000;
  }
  100% {
    color: #F7B500;
  }
}
.example-out {
  animation: bounce-out .5s;
}

</style>

六、显性的过渡持续时间

在很多情况下,Vue 可以自动得出过渡效果的完成时机。默认情况下,Vue 会等待其在过渡效果的根元素的第一个 transitionend 或 animationend 事件。然而也可以不这样设定——比如,我们可以拥有一个精心编排的一系列过渡效果,其中一些嵌套的内部元素相比于过渡效果的根元素有延迟的或更长的过渡效果。

在这种情况下你可以用 组件上的 duration prop 定制一个显性的过渡持续时间 (以毫秒计):

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

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

比如,下面代码我把动画的时间都去掉,把时间统一放在 组件上,同样可以达到效果

<template>
<div>
  <transition
    :duration="{ enter: 500, leave: 4000 }"
    enter-active-class="animate__animated animate__bounce"
    leave-active-class="example-out"
  >
    <h1 v-if="show">An animated element</h1>
  </transition>
  <button @click="show=!show">点击我试试</button>
</div>
</template>


<script>
  import Vue from 'vue'
  export default {
    components: {
    },
    data(){
      return {
        show:true
      }
    }
  }
</script>
<style>
@keyframes bounce-out {
  0% {vue
    color: #0000FF;
  }
  50% {
    color: #FF0000;
  }
  100% {
    color: #F7B500;
  }
}
.example-out {
  animation: bounce-out;
}

</style>

七、Javascript钩子

可以在 attribute 中声明 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) {
    // ...
  }
}

<template>
<div>
  <transition
    @before-enter="beforeEnter"
    @enter="enter"
    @after-enter="afterEnter"
    @enter-cancelled="enterCancelled"
  >
    <h1 v-if="show">An animated element</h1>
  </transition>
  <button @click="show=!show">点击我试试</button>
</div>
</template>

<script>
  import Vue from 'vue'
  export default {
    components: {
    },
    data(){
      return {
        show:true
      }
    },
    methods: {
      beforeEnter (el) {
        el.style.color='red';
      },
      enter (el,done) {
        el.style.color='blue';
        done();
      }
      ,
      afterEnter (el) {
        el.style.color='black';
      },
      enterCancelled (el) {
        //
      }
    }
  }
</script>

<style>
</style>

八、velocity的应用

velocity官方文档

JavaScript钩子里面可以写动画的逻辑,因此可以结合velocity这个js动画库,来实现更多的动画效果

首先安装

npm install velocity-animate --save-dev

在main.js中引入

import Velocity from 'velocity-animate'

<template>
<div>
  <transition
    @before-enter="beforeEnter"
    @enter="enter"
    @after-enter="afterEnter"
    @enter-cancelled="enterCancelled"
  >
    <h1 v-if="show">An animated element</h1>
  </transition>
  <button @click="show=!show">点击我试试</button>
</div>
</template>

<script>
  import Vue from 'vue'
  export default {
    components: {
    },
    data(){
      return {
        show:true
      }
    },
    methods: {
      beforeEnter (el) {
        el.style.opacity=0;
      },
      enter (el,done) {
        Velocity(el,{opacity:1},{duration:1000,complete:done})
      }
      ,
      afterEnter (el) {
        el.style.color='red';
      },
      enterCancelled (el) {
        //
      }
    }
  }
</script>
<style>

</style>

九、初始渲染过渡

初始渲染过渡即是页面初始化时便加载了过渡效果,可以通过 appear attribute 设置节点在初始渲染的过渡

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

这里默认和进入/离开过渡一样,同样也可以自定义 CSS 类名,下面是结合animate.css的例子

<template>
<div>
  <transition
  appear
  appear-class="animate__animated animate__bounce"
  appear-to-class="animate__animated animate__backInRight" (2.1.8+)
  appear-active-class="animate__animated animate__wobble"
  >
    <p >hello world</p>
  </transition>
</div>
</template>

<script>
  import Vue from 'vue'
  export default {
    components: {
    },
    data(){
      return {
      }
    }
  }
</script>
<style>

</style>

事实上,你会发现

appear-class和appear-to-class同时出现,只会显示apper-to-class的效果

appear--class和appear-active-class同时出现,是不会有效果的

appear-to-class和appear-active-class同时出现,只会显示appear-to-class的效果

因此,如果我们要实现初始过渡渲染,直接单独使用appear-active-class就好,但是appear是必须的

同样,也可以通过自定义 JavaScript 钩子来实现:

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

十、多个元素、组件的过渡

上面我们一直在讨论单个元素的初始化,插入,删除,更新的过渡效果,实际上,如果多个元素的切换显示也是可以加入过渡效果的,同样的道理,组件之间的切换也可以加入过渡效果

1、多个元素的过渡效果

首先来看看多个元素之间没有加入过渡效果是怎么样的

<template>
<div>
    <p v-if="tag">hello world</p>
    <p v-else>shit world</p>
    <button @click="tag=!tag">try</button>
</div>
</template>

<script>
  import Vue from 'vue'
  export default {
    components: {
    },
    data(){
      return {
        tag:true
      }
    }
  }
</script>
<style>

</style>

这里需要特别注意的问题是:

当有相同标签名的元素切换时,需要通过 key attribute 设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。因此,是无法看到元素切换的过渡效果的,即使在技术上没有必要,给在 组件中的多个元素设置 key 是一个更好的实践

<template>
<div>
  <transition
  enter-active-class="animate__animated animate__bounce"
  >
    <p v-if="tag" key="hello">hello world</p>
    <p v-else key="shit">shit world</p>
  </transition>
  <button @click="tag=!tag">try</button>
</div>
</template>

<script>
  import Vue from 'vue'
  export default {
    components: {
    },
    data(){
      return {
        tag:true
      }
    }
  }
</script>
<style>

</style>

在一些场景中,也可以通过给同一个元素的 key attribute 设置不同的状态来代替 v-if 和 v-else,上面的例子可以重写为:

<template>
<div>
  <transition
  enter-active-class="animate__animated animate__bounce"
  >
  <p v-bind:key="isEditing" >
    {{ isEditing ? 'Save' : 'Edit' }}
  </p>
  </transition>
  <button @click="isEditing=!isEditing">try</button>
</div>
</template>

<script>
  import Vue from 'vue'
  export default {
    components: {
    },
    data(){
      return {
        isEditing:true
      }
    },
    methods: {

    }
  }
</script>
<style>

</style>

使用多个 v-if 的多个元素的过渡可以重写为绑定了动态 property 的单个元素过渡。例如:

<template>
<div>
  <transition
  enter-active-class="animate__animated animate__bounce"
  >
    <p v-if="docState === 'saved'" key="saved">
      Edit
    </p>
    <p v-if="docState === 'edited'" key="edited">
      Save
    </p>
  </transition>
  <button @click="docState=='saved'?docState='edited':docState='saved'">try</button>
</div>
</template>

<script>
  import Vue from 'vue'
  export default {
    components: {
    },
    data(){
      return {
        docState:'saved'
      }
    },
    methods: {

    }
  }
</script>
<style>

</style>

可以重写为:

<template>
<div>
  <transition
  enter-active-class="animate__animated animate__bounce"
  >
  <p v-bind:key="docState">
    {{ message }}
  </p>
  </transition>
  <button @click="docState=='save'?docState='edit':docState='save'">try</button>
</div>
</template>

<script>
  import Vue from 'vue'
  export default {
    components: {
    },
    data(){
      return {
        docState:'save'
      }
    },
    computed: {
      message: function () {
        switch (this.docState) {
          case 'save' : return 'edit'
          case 'edit' : return 'save'
        }
      }
    },
    methods: {

    }
  }
</script>
<style>

</style>

2、过渡模式

的默认行为 - 进入和离开过渡效果同时发生

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

  • in-out:新元素先进行过渡,完成之后当前元素过渡离开。
  • out-in:当前元素先进行过渡,完成之后新元素过渡进入。

3、多个组件的过渡

多个组件的过渡简单很多 - 我们不需要使用 key attribute。相反,我们只需要使用动态组件:

<template>
<div>
  <button
    v-for="tab in tabs"
    v-bind:key="tab"
    v-bind:class="['tab-button', { active: currentTab === tab }]"
    v-on:click="currentTab = tab"
  >
    {{ tab }}
  </button>
  <transition
  enter-active-class="animate__animated animate__backInUp"
  >
    <component v-bind:is="currentTabComponent" class="tab"></component>
  </transition>
</div>
</template>

<script>
  import Vue from 'vue';
  Vue.component("tab-home", {
    template: "<div>Home component</div>"
  });
  Vue.component("tab-posts", {
    template: "<div>Posts component</div>"
  });
  Vue.component("tab-archive", {
    template: "<div>Archive component</div>"
  });
  export default {
    data(){
      return {
        currentTab: "Home",
        tabs: ["Home", "Posts", "Archive"]
      }
    },
    computed: {
      currentTabComponent: function() {
        return "tab-" + this.currentTab.toLowerCase();
      }
    }
  }
</script>
<style>
  .tab-button {
    padding: 6px 10px;
    border-top-left-radius: 3px;
    border-top-right-radius: 3px;
    border: 1px solid #ccc;
    cursor: pointer;
    background: #f0f0f0;
    margin-bottom: -1px;
    margin-right: -1px;
  }
  .tab-button:hover {
    background: #e0e0e0;
  }
  .tab-button.active {
    background: #e0e0e0;
  }
  .tab {
    border: 1px solid #ccc;
    padding: 10px;
  }
</style>

十一、列表过渡

列表数据渲染怎么实现过渡效果?比如,使用v-for这种场景

vue提供了 组件,在我们深入例子之前,先了解关于这个组件的几个特点:

  • 不同于 ,它会以一个真实元素呈现:默认为一个 。你也可以通过 tag attribute 更换为其他元素,最终渲染的节点元素是设置的那个

  • 过渡模式不可用,因为我们不再相互切换特有的元素。

  • 内部元素总是需要提供唯一的 key attribute 值。

  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。

十二、动画组件封装

将动画封装成一个组件,就意味着所有的动画效果都封装在一个组件中,每次使用动画组件时,只需要将需要加载动画的元素通过插槽传递给动画组件即可

<template>
<div>
  <fade :show="isShow"><div>hello world</div></fade>
  <button @click="isShow=!isShow">点击我试试</button>
</div>
</template>

<script>
  import Vue from 'vue'
  Vue.component('fade',{
    props:['show'],
    template:`
    <div>
      <transition
        @before-enter="beforeEnter"
        @enter="enter"
      >
        <slot v-if="show"></slot>
      </transition>
    </div>
    `,
    methods:{
      beforeEnter (el) {
        el.style.color='green';
      },
      enter (el,done) {
        setTimeout(() => {
          el.style.color='red';
          done();
        }, 2000);
      }
    }
  })
  export default {
    // components: {
    //   fade
    // },
    data(){
      return {
        isShow:true
      }
    },
    methods: {
      beforeEnter (el) {
        el.style.fontSize='30px';
        el.style.color='red';
      },
      enter (el,done) {
        el.style.fontSize='14px';
        el.style.color='blue';
        done();
      }
    }
  }
</script>

<style>
</style>