别再说Transition 无法过渡display了

8,296 阅读5分钟

Vue Transition

事件起因

因需求关系,要求做到添加购物车时,有个小球从点击添加的按钮通过运动轨迹到达购物车位置, 因为是需要获取动态位置, 通过css渲染动画心有余而力不足, 很幸运的是, vue-transition内置组件提供js钩子函数, 为我提供完成的动画的保障

实践

首先, 让我们了解下vue transition的一些特性:

Props:
    name - string,用于自动生成 CSS 过渡类名。例如:name: 'fade' 将自动拓展为.fade-enter,.fade-enter-active等。默认类名为 "v"
    appear - boolean,是否在初始渲染时使用过渡。默认为 false。
    css - boolean,是否使用 CSS 过渡类。默认为 true。如果设置为 false,将只通过组件事件触发注册的 JavaScript 钩子。
    type - string,指定过渡事件类型,侦听过渡何时结束。有效值为 "transition""animation"。默认 Vue.js 将自动检测出持续时间长的为过渡事件类型。
    mode - string,控制离开/进入的过渡时间序列。有效的模式有 "out-in""in-out";默认同时生效。
    duration - number | { enter: number, leave: number } 指定过渡的持续时间。默认情况下,Vue 会等待过渡所在根元素的第一个 transitionend 或 animationend 事件。
    enter-class - string
    leave-class - string
    appear-class - string
    enter-to-class - string
    leave-to-class - string
    appear-to-class - string
    enter-active-class - string
    leave-active-class - string
    appear-active-class - string
事件:
    before-enter
    before-leave
    before-appear
    enter
    leave
    appear
    after-enter
    after-leave
    after-appear
    enter-cancelled
    leave-cancelled (v-show only)
    appear-cancelled

具体的一些使用方法,因为与本文内容不是很贴边,在这里不详细讲了 剩余的就不在这里说了, 开始主题

举个例子更加让人了解问题

不知道大家在写原生js的时候,有没有遇到过这样的一个问题

一个DOM元素从display: none变为display: block的时候,想要它想vue的transition一样,利用动画过渡过来, 你很有可能会这么写

    <div class="c-transition__container" style="display: none;"></div>
    .c-transition__container {
        width: 100px;
        height: 100px;
        background: red;
        transition-duration: .5s;
        transition-property: transform;
    }
    const oDiv = document.getElementsByClassName('c-transition__container')[0]
    document.onclick = function() {
        oDiv.style.transform = 'translate3d(100px, 0, 0)'
        oDiv.style.display = 'block'
    }

事实总是难以接受, 这么写是不会有平滑过渡的效果,而是直接在最终的位置显示

沮丧的你从此弃坑,发誓再也不碰前端:laughing:

别着急, 下面讲解如何实现从0到1的平滑

首先我们要了解浏览器的渲染时机

    for(let i = 0; i < 100; i++) {
        oDiv.style.transform = 'translate(100px, 0, 0)'
    }

以上的for循环语句里操作了100次的div, 但实际上浏览器并不会渲染100次, 根据上文

你理解错误的Vue nextTick

我们可以得知, 浏览器的UI Render是在每个macroTask最后清空microTask队列后才会触发一次, 但是浏览器会根据实际情况来确定是否需要渲染, 通常在每隔16.7ms的情况下会渲染一次, 在此期间,浏览器会将所有的DOM操作推入到队列中,在进行渲染的时候会一个一个取出,直到清空队列。

浏览器渲染过程:

JavaScript:JavaScript 实现动画效果,DOM 元素操作等。(Cpu)

Style(计算样式):确定每个 DOM 元素应该应用什么 CSS 规则。(Cpu)

Layout(布局):计算每个 DOM 元素在最终屏幕上显示的大小和位置。由于 web 页面的元素布局是相对的,所以其中任意一个元素的位置发生变化,都会联动的引起其他元素发生变化,这个过程叫 reflow。(每个DOM对应一个渲染层)(Cpu)

Paint(绘制):在多个层上绘制 DOM 元素的的文字、颜色、图像、边框和阴影等, 这个过程叫做repaint。(Cpu)

Composite(渲染层合并):按照合理的顺序合并图层然后显示到屏幕上。(进入GPU)(render Tree 解析渲染)

理解了以上部分, 那么接下来的问题就好办了

我们逐句分析刚才的案例:

  1. 首先我们初始化了div的一些基本状态, 包括宽高, transition等一些css样式
  2. 在点击document的时候,我们将div的style复制为了translate(100px, 0, 0),并且更新了状态display: block, 注意这个时候, 也就是我刚才提到的浏览器, 并没有进行直接进行渲染,而是将它推入到了队列里, 但记得一句, DOM Tree是实时更新的, 此时虽然没有渲染,但是DOM Tree里的状态已经更新了,这个时候DOM transform已经变成了100px, 当真正渲染的时候, 是从100px变为100px,并不是从0变成100px,所以我们看到的是没有过渡效果的。

解决方案

从以上的分析,我们可以基本了解为什么会是这种现象, 下面我来讲下解决方案:

  1. 首先就是大家所熟知的setTimeout,这个的确可以解决以上问题, 但是setTimeout属于macroTask, 每个task的最后都会触发一次UI Render, 以上操作会造成多次渲染, 所以并不推荐使用

  2. (推荐) 强制浏览器清空队列进行渲染操作

    在获取布局信息操作的时候, 会强制浏览器清空队列进行渲染, API有以下几个: innerHeight、scrollTop、offsetHeight、getBoundingClientRect等等

    这个时候, 浏览器重新进行回流重绘渲染操作, 这个时候触发DOM操作都可以得到有效回馈

总结

Vue transitionjs钩子函数其实就是原生js写的动画, 同样从无到有进行平滑, 你学会了吗?