Vue文档阅读——过渡&动画

183 阅读9分钟

离开/进入&列表过渡

概述

Vue在插入,更新或者移除DOM时,提供多种方式过渡效果:

  • 在css过渡动画中自动应用class
  • 配合第三方css动画库,animate.css
  • 在过渡钩子函数中使用js直接操作DOM
  • 配合第三方js动画库,velocity.js

单元素/组件的过渡

Vue提供了transition的封装组件,在以下情况可以添加进入/离开过渡:

  • 条件渲染v-if
  • 条件展示v-show
  • 动态组件
  • 组件根节点
<div id="demo">
  <button v-on:click="show = !show">
    Toggle
  </button>
  <transition name="fade">
    <p v-if="show">hello</p>
  </transition>
</div>
.fade-enter-active, .fade-leave-active {
  transition: opacity .5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}

当插入或删除包含在transition中的元素时,Vue会:

  1. 自动嗅探目标元素是否应用了css过渡动画,如果是,就在恰当的时机添加/删除css类名。
  2. 如果过渡组件提供了js钩子函数,就会在恰当时机调用。
  3. 如果没有js钩子也没有css过渡,DOM操作在下一帧立即执行。

过渡的类名

在过渡中,有6个class切换:

  1. v-enter:定义进入过渡的开始状态。在元素被插入之前生效,被插入之后的下一帧就移除。
  2. v-enter-active:定义进入过渡生效时的状态。
  3. v-enter-to:定义进入过渡的结束状态。
  4. v-leave:定义离开过渡的开始状态。
  5. v-leave-active:定义离开过渡生效时的状态。
  6. v-leave-to:定义离开过渡的结束状态。

如果transition没有名字,v-就是默认前缀。

CSS过渡

常用的过渡就是CSS过渡:

<div id="example-1">
  <button @click="show = !show">
    Toggle render
  </button>
  <transition name="slide-fade">
    <p v-if="show">hello</p>
  </transition>
</div>
/* 可以设置不同的进入和离开动画 */
/* 设置持续时间和动画函数 */
.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 for below version 2.1.8 */ {
  transform: translateX(10px);
  opacity: 0;
}

CSS动画

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

<div id="example-2">
  <button @click="show = !show">Toggle show</button>
  <transition name="bounce">
    <p v-if="show">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis enim libero, at lacinia diam fermentum id. Pellentesque habitant morbi tristique senectus et netus.</p>
  </transition>
</div>
.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);
  }
}

自定义过渡的类名

通过attribute改变类名:

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

同时使用过渡和动画

可以设置响应的事件监听器,监听transitionend和animationend,来让Vue知道过渡完成了。

如果既有过渡又有动画,可以使用type attribute并设置为animation或者transition来明确声明你要监听的类型。

显性的过渡持续时间

一般Vue可自动得到过渡效果的完成时机。在transition上duration属性定制一个显性的过渡持续事件

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

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

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

JavaScript钩子

可以在属性中声明js钩子:

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

当只用js过渡的时候,在enter,leave中必须使用done进行回调。否则会立刻完成。

与第三方库velocity结合的例子:

<!--
Velocity 和 jQuery.animate 的工作方式类似,也是用来实现 JavaScript 动画的一个很棒的选择
-->
<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
      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 })
    }
  }
})

初始渲染的过渡

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

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

多个元素的过渡

最常见的多标签过渡是一个列表和描述这个列表为空消息的元素:

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

需要注意:当有相同标签名的元素切换时,需要通过key attribute设置唯一的值让Vue区分它们。

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

也可以动态绑定key:

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

过渡模式

有时候transition的默认行为——进入和离开同时发生,所以Vue提供了过渡模式:

  • in-out:新元素先进行过渡,完成之后当前元素过渡离开。
  • out-in:当前元素先进行过渡,完成之后新元素过渡进入。
<transition name="fade" mode="out-in">
  <!-- ... the buttons ... -->
</transition>

in-out不常用。

多个组件的过渡

只需要使用动态组件:

<transition name="component-fade" mode="out-in">
  <component v-bind:is="view"></component>
</transition>
.component-fade-enter-active, .component-fade-leave-active {
  transition: opacity .3s ease;
}
.component-fade-enter, .component-fade-leave-to
/* .component-fade-leave-active for below version 2.1.8 */ {
  opacity: 0;
}

列表过渡

如何同时渲染整个列表,比如使用v-for。

使用组件:

  • 不同于transition,它会以一个真实元素呈现,默认是span,可通过tag attribute更换
  • 过渡模式不可用,因为我们不再相互切换
  • 需要提供唯一的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>

列表的排序过渡

还有一个特殊点,不仅可以进入和离开动画,还可以改变定位。

新增的v-move attribute会在元素改变定位的过程中应用。

<transition-group name="flip-list" tag="ul">
  <li v-for="item in items" v-bind:key="item">
    {{ item }}
  </li>
</transition-group>
.flip-list-move {
transition: transform 1s;
}

其内部实现: Vue使用了一个叫FLIP简单的动画队列,使用transform将元素从之前的位置平滑过渡新的位置。

FLIP过渡的元素不能设置为display:inline.

FLIP动画不仅可以实现单列,也可用于网格。

列表的交错过渡

使用data属性和js通信,实现查询过渡:

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

可复用的过渡

过渡可以通过Vue的组件系统实现复用,要创建一个可复用的过渡组件,就把或者作为根组件即可。

动态过渡

最基本例子就是通过name attribute绑定动态值:

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

当你想用Vue的过渡系统来定义CSS切换会很有用。

所有过渡attribute都可以动态绑定,但还可以通过事件钩子获取上下文中所有数据,因为事件钩子都是方法。根据组件状态不同,js过渡会有不同表现:

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

创建动态过渡的最终方案是组件通过接受props来动态修改之前的过渡。

状态过渡

Vue的过渡系统对于数据元素本身的动效:

  • 数字的运算
  • 颜色显式
  • SVG节点
  • 元素大小和其他属性

这些数据用数值结合响应式和组件实现过渡。

状态动画与侦听器

通过侦听器我们可以监听到任何数值属性的更新,使用GreenSock:

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js"></script>

<div id="animated-number-demo">
  <input v-model.number="number" type="number" step="20">
  <p>{{ animatedNumber }}</p>
</div>
new Vue({
  el: '#animated-number-demo',
  data: {
    number: 0,
    tweenedNumber: 0
  },
  computed: {
    animatedNumber: function() {
      return this.tweenedNumber.toFixed(0);
    }
  },
  watch: {
    number: function(newValue) {
      TweenLite.to(this.$data, 0.5, { tweenedNumber: newValue });
    }
  }
})

如果对于不能直接像数字一样存储的值的话,使用Tween.js和Color.js:

<script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script>
<script src="https://cdn.jsdelivr.net/npm/color-js@1.0.3"></script>

<div id="example-7">
  <input
    v-model="colorQuery"
    v-on:keyup.enter="updateColor"
    placeholder="Enter a color"
  >
  <button v-on:click="updateColor">Update</button>
  <p>Preview:</p>
  <span
    v-bind:style="{ backgroundColor: tweenedCSSColor }"
    class="example-7-color-preview"
  ></span>
  <p>{{ tweenedCSSColor }}</p>
</div>
var Color = net.brehaut.Color

new Vue({
  el: '#example-7',
  data: {
    colorQuery: '',
    color: {
      red: 0,
      green: 0,
      blue: 0,
      alpha: 1
    },
    tweenedColor: {}
  },
  created: function () {
    this.tweenedColor = Object.assign({}, this.color)
  },
  watch: {
    color: function () {
      function animate () {
        if (TWEEN.update()) {
          requestAnimationFrame(animate)
        }
      }

      new TWEEN.Tween(this.tweenedColor)
        .to(this.color, 750)
        .start()

      animate()
    }
  },
  computed: {
    tweenedCSSColor: function () {
      return new Color({
        red: this.tweenedColor.red,
        green: this.tweenedColor.green,
        blue: this.tweenedColor.blue,
        alpha: this.tweenedColor.alpha
      }).toCSS()
    }
  },
  methods: {
    updateColor: function () {
      this.color = new Color(this.colorQuery).toRGB()
      this.colorQuery = ''
    }
  }
})
.example-7-color-preview {
  display: inline-block;
  width: 50px;
  height: 50px;
}

动态状态过渡

把过渡放到组件里

管理太多的状态过渡会增加复杂性

赋予设计以生命

使用svg,发挥想象,让其动起来