Vue - 过渡 & 动画

2,628 阅读9分钟

进入/离开 & 列表过渡

Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。包括以下工具:

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

这里,只讲进入、离开和列表的过渡

单元素组件过渡

Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡

  • 条件渲染 (使用 v-if )
  • 条件展示 (使用 v-show )
  • 动态组件
  • 组件根节点

过渡的类名 & CSS过渡

常用的过渡都是使用 CSS 过渡,插入或删除包含在 transition 标签中的元素时,会对这个元素添加和移除 进入/离开 的6个class类名来完成动画

  1. v-enter:定义元素进入的初始状态
  2. v-enter-active:定义元素进入过渡动画的 transition
  3. v-enter-to:定义元素进入动画结束时的状态
  4. v-leave:定义元素离开的初始状态
  5. v-leave-active:定义元素离开过渡动画的 transition
  6. v-leave-to:定义元素离开动画结束时的状态
  • 上方提供了四个时间点元素的状态,如果元素的过渡是 从哪儿来的回哪儿去进入前状态离开后状态 相同,进入后状态离开前状态 相同,那么css可以写在一起
.v-enter,.v-leave-to{ //元素进入前和离开结束时的状态
    //...
}
.v-enter-to,.v-leave{ //这组css会在过渡结束后被移除,立即恢复,没有过渡动画
    //...             //所以为了保证有过渡,元素本身也需要有transition才能有过渡动画来恢复初始样式
}
  • v-enter-activev-leave-active 用来定义进入和离开动画的 transition ,同样的也是可以写到一起的
.v-enter-active,.v-leave-active{
    //...
}

使用 <transition> 标签来包裹需要过渡的元素,v- 是这些类名的默认前缀。标签内写入name属性并命名 <transition name="my-transition">,那么 v-enter 要写成 my-transition-enter ,来为这类元素单独定制过渡动画

CSS动画

CSS 动画用法同 CSS 过渡,区别是在动画中 v-enter 类名不会立即删除,而是在动画结束时删除

<div>
  <button @click="show = !show">Toggle show</button>
  <transition>
    <p v-if="show">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
  </transition>
</div>
new Vue({
  data: {
    show: true
  }
})
.v-enter-active {
  animation: bounce-in .5s;
}
.v-leave-active {
  animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.5);
  }
  100% {
    transform: scale(1);
  }
}

自定义过渡类名

  • enter-class
  • enter-active-class
  • enter-to-class
  • leave-class
  • leave-active-class
  • leave-to-class

他们的优先级高于普通的类名,这对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 Animate.css 结合使用十分有用。

<link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css">

<div>
  <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({
  data: {
    show: true
  }
})

同时使用过渡 & 动画

Vue 提供了 transitionendanimationend 的事件监听器,Vue 能自动识别类型并设置监听

但是,在一些场景中,需要给同一个元素同时设置两种过渡动效,比如 animation 很快的被触发并完成了,而 transition 效果还没结束。在这种情况中,你就需要使用 type 属性并设置 animationtransition 来明确声明你需要 Vue 监听的类型

显性的过渡持续时间

可以用 <transition> 组件上的 duration prop 定制一个显性的过渡持续时间 (以毫秒计):

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

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

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

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) {
    // ...
  }
}

这些钩子函数可以结合 CSS transitions/animations 使用,也可以单独使用

当只用 JavaScript 过渡的时候,enterleave 中必须使用 done 进行回调,否则,它们将被同步调用,过渡会立即完成

推荐对于仅使用 JavaScript 过渡的元素添加 v-bind:css="false",Vue 会跳过 CSS 的检测,这也可以避免过渡过程中 CSS 的影响

多个元素的过渡

单个节点,同时间渲染多个节点中的一个,对于原生标签可以使用 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 attribute 设置不同的状态来代替 v-ifv-else,上面的例子可以重写为:

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

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

<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'
    }
  }
}

过渡模式

多个元素之间切换,进入和离开同时发生,是 <transition> 的默认行为,Vue 提供了过渡模式

  • out-in:当前元素离开后,新元素再进入
  • in-out:新元素先进入,完成后当前元素离开,这个模式不常用,只是提供了一个可能
<transition name="fade" mode="out-in">
  <!-- ... the buttons ... -->
</transition>

多个组件过渡

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

<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{
  opacity: 0;
}

列表过渡

同时渲染整个列表,比如使用 v-for 在这种场景中,使用 <transition-group> 组件,关于这个组件的几个特点:

  • 不同于 <transition>,它会以一个真实元素呈现:默认为一个 <span>,也可以通过 tag 属性更换为其他元素
  • 过渡模式不可用,因为我们不再相互切换特有的元素
  • 内部元素总是需要提供唯一的 key 属性值
  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身

列表的进入/离开过渡

<transition-group name="list" tag="p">
    <span v-for="item in items" v-bind:key="item" class="list-item">
      {{ item }}
    </span>
</transition-group>

当添加和移除元素的时候,周围的元素会瞬间移动到他们的新布局的位置,而不是平滑的过渡,下面会解决这个问题

列表的排序过渡

<transition-group> 组件还有一个特殊之处,不仅可以进入和离开动画,还可以改变定位, v-move class会在元素的改变定位的过程中应用,使用 transforms 将元素从之前的位置平滑过渡新的位置,像之前的类名一样,可以通过 name 属性的值来自定义前缀,也可以通过 move-class 属性手动设置

元素不能设置为 display: inline,作为替代方案,可以设置为 display: inline-block 或者放置于 flex 中,v-move下面要给v-leave-active加上absolute定位,来解决移除后元素没有动画的问题,如果因为absolute定位导致元素宽高异常再看情况解决

.v-move {
	transition: 0.3s ease-in-out;
}
.v-leave-active { 
	position: absolute;
	height: 100%;
}

可复用的过渡

过渡可以通过 Vue 的组件系统实现复用,要创建一个可复用过渡组件,就是将 <transition> 或者 <transition-group> 作为根组件,然后将任何子组件放置在其中就可以了

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 中即使是过渡也是数据驱动的,动态过渡最基本的例子是通过 name attribute 来绑定动态值

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

当你想用 Vue 的过渡系统来定义的 CSS 过渡/动画在不同过渡间切换会非常有用

所有过渡 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" 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
          }
        }
      )
    }
  }
})

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