谈谈你对CSS、JS、Vue动画的理解

392 阅读12分钟

本篇文章的重点不在具体语法上的细节,这些我们查MDN便知,而是希望从浏览器渲染原理角度来说说这些动画具体的特点,及其应用场景,怎样使用更利于页面性能等。

CSS动画:


CSS动画比较简单,有两种设置的方法一种是transition一种是animation

transition:

首先来看一个简写写法,一般接收四个参数:

  • property name:要进行过渡的属性,如left,height,如果懒得分别直接写all也可以
  • duration:动画持续的时间
  • timing function:动画的运动曲线,取值liner,ease,ease-in,ease-out,ease-in-out
  • delay:延迟动画运动时间

利用transition在设置动画时只有两个关键帧,起始帧结束帧。页面初始渲染时的状态可以作为起始帧,而结束帧的获取我们需要让需要添加动画的元素得到新的样式便可。

利用CSS的伪类(:hover,:focus等):我们在触发相应的事件时,元素就会添加上对应的伪类从而得到新的样式,新的样式作为结束帧,之后浏览器计算两者之间的差异完成过渡动画。

我们可以看一个简单的案例,也是一道字节的面试题,要求实现一个滑动按钮,鼠标悬浮时自动向右滑动且只使用一个dom元素:

// html
<div class="box"></div>

// css
.box {
  position: relative;
  width: 100%;
  height: 50px;
  background-color: blue;
}

.box::after {
  position: absolute;
  left:0;
  top:0;
  content:'';
  display:block;
  width: 20%;
  height: 100%;
  background-color: red;
  transition:left 1s;
}

.box:hover::after {
  left:80%;
}


  image.png
效果就是当鼠标悬浮在.box元素之上时红色的块伪元素会向右移动,当鼠标离开时再回到初始的位置。需要注意的是我们设置过渡的属性是left,对于绝对定位的元素来说其是被渲染在单独的图层之上的,所以并不会造成整个页面的回流。在实际使用transition过渡动画时应避免使用像height这样会导致这个页面回流的属性。

除了利用CSS添加伪类的方式,我们也可以通过JS给元素添加新的类名的方式来生成动画,和上面的原理是一样的就不写案例了。

注意transition的主要特点是其只要两个关键帧,比较适合只有两个状态切换的场景,比如鼠标的悬浮,像a标签时鼠标浮动在其上时改变颜色。

animation:


如果我们对动画的需求更为复杂一点那么两个关键帧很明显不够用,所以我们有了animation来获得更多的帧。

首先还是基本语法,这里只提供一个简写方式,像了解更多还是请看animation MDN

animation: 动画名称 | 持续时间 | 运动曲线 | 何时开始 | 播放次数(infinite无限循环) |
是否在下一周期反向播放(默认normal或者alternate) |
规定刚开始动画的状态是否暂停(默认是running可以是paused暂停)


虽然参数比较多,但前面几个比较常用的像动画名称,持续时间,运动曲线,延迟执行的时间,是否无限循环,还有最后一个初始状态是否暂停这些还是可以记一记。

其一般配合@keyframes使用,中间可以通过百分比来控制更多的过程帧:

@keyframes loading {
   0% {
     transform:rotate(0deg);
   }
    
  50% {
     transform:rotate(180deg);
   } 	
  
   100% {
     transform:rotate(360deg);
   }
}


总结一下与transition的区别就是可以得到更多的过程帧了,从而完成更加复杂的动作,触发方式也不一定需要通过什么事件,可以自己触发。不依赖与用户的交互,更适合去完成一些独自在页面上重复的动作。

这里有一个环形加载动画案例大家可以看一下:

<div class="container">
  <div class="loading"></div>
</div>
.container {
  position: relative;
  width: 200px;
  height: 200px;
  background: rgba(0,0,0,0.5);
}

.loading {
  position: absolute;
  width: 100px;
  height: 100px;
  top:0;
  left:0;
  bottom:0;
  right:0;
  margin:auto;
  border-radius: 50%;
  border:5px solid blue;
  border-top-color: white;
  animation: loading 1.5s ease-in-out infinite;
}

@keyframes loading {
   0% {
     transform:rotate(0deg);
   }
    
  
   100% {
     transform:rotate(360deg);
   }
}

image.png



总结:


CSS动画的特点就是简单好用,我们将CSS代码写好便可以专心去写JS代码了,对于一些简单的动作能使用CSS来完成的尽量还是使用CSS。

transition用来完成两个状态切换之间的动画,而animation为我们提供了更多的状态帧,可以完成更加复杂的动作。但是说了这么多大家最关心的应该还是这两个动画的性能怎么样?会导致回流,重绘吗?

其是我最开始想写这篇文章的目的就是想好好了解一下关于动画对浏览器性能的影响,如果会造成大量的回流还是少用为好,但是在查阅很多文章之后发现,其实CSS这两个动画其本身对浏览器渲染的影响主要还是取决与我们到底设置的什么属性。

比如第一个案例中,我们控制的是带定位伪元素的left,其本身是单独的图层,那么也只是造成小范围的回流罢了,但是如果我们设置是height这样会改变元素几何尺寸的属性,那么便会造成整个页面的回流,这是非常不可取的。在第二个案例中我们修改的是元素的transform属性,其不参与DOM tree与Layout tree的生成,直接由合成线程发送图块给栅格化线程池调用GPU加速生成**位图数据,**相反比较耗时的是将位图数据存入缓存的过程,但也可忽略。对于浏览器渲染页面过程感兴趣的同学可以看我带你看看从输入URL到页面显示背后的故事这篇万字文章。

所以在使用CSS设置动画的时候,应该尽量设置那些对页面性能比较友好的属性,如CSS3的transform,opacity,filter,还有absolute,fixed,relative的index不为auto的这些定位属性。

JS动画:


JS动画实现的方式非常多样,其最大的优点是强化了对动画过程的控制,使用JS可以更好的监听动画当前运动状态,配合页面交互等实现更丰富的效果。

利用定时器:


假设我们想让一个div动起来,我们可以通过在很短的时间内多次修改定位元素的left而得到(当然也有其他方法,我认为直接修改定位元素的left是比较方便且性能较好的一种方法)。

任何动画的本质都是让其在很短的时间内多次变化,从而让我们的大脑误以为其是真实运动。


我们可以借助setIterval来在很短的时间内来多次改动元素的left,下面是我封装的一个比较简单的方法。

function animate(target, positionTarget, callback) {
    // 防止多次添加定时器造成叠加问题
    clearInterval(target.timer);
    target.timer = setInterval(function () { //给对象添加方法的形式添加定时器
        if (target.offsetLeft >= positionTarget) {
            clearInterval(target.timer);
            if (callback) {
                callback(); //在定时器停止时调用
            }
        }
        target.speed = (positionTarget - target.offsetLeft) / 20; //此公式达到速度递减的作用
        // 向下取整保证精度
        target.speed = target.speed > 0 ? Math.ceil(target.speed) : Math.floor(target.speed);
        target.style.left = target.offsetLeft + target.speed + 'px'; //通过成比例的减小步长而达到控制精度的目的
    }, 15); //设置成每隔15毫秒触发一次(推荐)
}


借助其我们完成一个简单的案例:

    <style>
        .box {
            position: absolute;
            top:0;
            left:0;
            width: 100px;
            height: 100px;
            background-color: red;
        }
    </style>
    <div class="box">
    </div>
    <script src="./animate.js"></script>
    <script>
        let myBox = document.querySelector('.box');
        animate(myBox,1000)
    </script>


image.png

图片会以逐渐减慢的速度移动向右边,大家可以自己试试。

利用requestAnimationFrame:


虽然我们利用定时器实现了比较好的效果,但是定时器属于异步宏任务,其执行时间并不一定是准确的,如果在执行异步宏任务前在异步微任务队列有较多的任务等待去处理那么,定时器触发的异步宏任务的回调函数一定会延迟执行,那么对于用户来说会感觉到动画出现卡顿的现象,也就是所谓的掉帧。

基于此,我们便有了一个专门为动画而创造的APIrequestAnimationFrame,其作用类似于定时器setInterval。要说清楚requestAnimationFram我们还要再了解一些关于浏览器事件环的知识。

我们知道在一个Event Loop中,每次所有的微任务队列清空后浏览器便会进行一次DOM渲染工作,PS:这也是为什么Vue里DOM的更新一定是在微任务里执行,因为可以尽快在页面中得到更新后的DOM。

所以说DOM的变动浏览器并不是会马上去渲染,这取决于浏览器认为是否有必要。

  • 如果我们在一个事件环内多次修改同一DOM那么浏览器便会将变动保留下来只进行一次渲染
  • 如果在一帧的时间内(约16.7ms)有多处DOM进行变动,那么浏览器便会将变动保留下来只进行一次渲染


也就是说如果我们希望在每次事件环中都进行DOM的渲染,那么我们就将DOM的修改放在requestAnimationFrame的回调函数中,如果我们递归调用requestAnimationFrame其会以一秒60次的速度执行其回调函数。

我们来看一个小例子来帮助大家理解requestAnimationFrame的用法:

    <div id="demo" style="position:absolute; width:100px; height:100px; background:#ccc; left:0; top:0;"></div>
    <script>
        var demo = document.getElementById('demo');
        function rander(){
            demo.style.left = parseInt(demo.style.left) + 20 + 'px'; //每一帧向右移动1px
        }
        requestAnimationFrame(function(){
            rander();
            //当超过300px后才停止
            if(parseInt(demo.style.left)<=1000){	
                requestAnimationFrame(arguments.callee);
            }
        });
    </script>

image.png

图中的灰色方块会丝滑的移动向右边。

总结:


说了这么多对于JS实现动画这里总共提供了两种方案,原理都是快速的修改元素的left让元素有移动的效果,对于定时器实现来说其有较大的延迟,如果在一个事件环中有较多的微任务要去处理那么很可能会出现动画卡顿或掉帧的现象,使用requestAnimationFrame实现的动画的性能是更好的,但有些时候可能会出现一些兼容性问题,IE10以上才支持,我们要更据需要来灵活选用。

Vue动画:


关于Vue动画具体语法我想大家查看文档便知,这里就与大家再一起过一遍。

首先对于添加动画的元素Vue是有要求的,在以下几种情况下才可以添加动画:

  • v-if
  • v-show
  • 动态组件
  • 组件根节点


我们需要将添加动画的元素包裹在transition标签里,之后通过让Vue在恰当的时机添加不同类名的方法来实现动画效果,这其实也就是Vue动画的原理,官网清楚的告诉了我们。

当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理

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

添加类名的方式:


接下来看几个例子吧(Vue基础好的直接跳过便好),首先是最简单的添加类名的方式:

    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .box {
            width: 200px;
            height: 200px;
            background-color: darkgreen;
        }

        .one-enter {
            /* opacity指变化的程度 */
            opacity: 0;
            margin-left: 100px;
        }

        .one-enter-to {
            opacity: 1;
            margin-left: 0;
        }

        .one-enter-active {
            transition: 1s all;
        }

        .two-leave {
            opacity: 1;
        }

        .two-leave-to {
            opacity: 0;
            margin-top: 100px;
        }

        .two-leave-active {
            transition: 1s all;
        }
    </style>
    <div id='app'>
        <button @click="toggle">我是按钮</button>
        <transition appear name="one">
            <div class="box" v-show="isShow"></div>
        </transition>
        <transition appear name="two">
            <div class="box" v-show="isShow"></div>
        </transition>
    </div>
    <script>
        let vue = new Vue({
            el: '#app',
            data: {
                isShow: true
            },
            methods: {
                'toggle': function () {
                    this.isShow = !this.isShow;
                }
            }
        });
    </script>

  image.png

  • 我们可以自己控制动画类名的前缀,通过transition标签上的name属性,如果不设定的话前缀默认是v-
  • appear属性用来控制页面在初始渲染时是否触发动画效果
  • 关于v-enter,v-enter-to,v-enter-active类名插入的时机(v-leave,v-leave-to,v-leave-active也一样)
  1. v-enter:在元素被插入前有效,插入后下一帧失效
  2. v-enter-to:元素被插入后下一帧有效(v-enter失效)
  3. v-enter-active:这个类在元素被插入前就有效了,在元素完成过渡后失效,一般我们用来写元素用来过渡的时间,等如transition:all 1s

利用钩子函数:

    <!-- 注意点:
    1.一个transition标签只能放一个元素
    2.如果需要在刷新页面时就显示动画效果可以给transition添加appear属性
    3.可以给transition标签添加name=""属性修改类名前缀 -->
    <div id='app'>
        <button @click="toggle">我是按钮</button>
        <transition appear  
        v-bind:css="false" 
        v-on:before-enter="beforeEnter" 
        v-on:enter="enter"
        v-on:after-enter="afterEnter">
        <div class="box" v-show="isShow"></div>
        </transition>
    </div>
    <script>
        let vue = new Vue({
            el: '#app',
            data: {
                isShow: true
            },
            methods: {
                'toggle': function () {
                    this.isShow = !this.isShow;
                },
                'beforeEnter': function (el) {
                    el.style.opacity = 0;
                    el.style.marginLeft = "0";
                },
                'enter': function (el, done) {
                    el.offsetWidth;
                    el.style.transition = "3s all";
                    // 如果需要一刷新网页就有动画,需要将done()方法延时执行
                    setTimeout(function () {
                        done();
                    }, 0)
                },
                'afterEnter': function (el) {
                    el.style.opacity = "1";
                    el.style.marginLeft = "500px";
                }
            }
        });
    </script>

注意

  • 如果我们要使用钩子函数添加类名,我们需要在transition标签上添加:css="``false",来让在style中设置的过渡效果不再起作用。
  • 我们需要在enter或者leave的回调中调用done(),并且如果我们还添加了appear属性我们需要让done()方法延时执行,最好放在一个定时器中

动画列表:


上面我们都是将需要添加动画的元素放在transition中,需要注意的是transition中只能放一个元素,如果我们需要给多个元素添加动画效果我们需要使用transition-group,在真正渲染时transition-group会被默认替换为一个apan标签,如果我们想要渲染成别的标签可以通过tag="tagName"来设置。

    <style>
        .v-enter {
            opacity: 0;
        }

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

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

        .v-leave {
            opacity: 1;
        }

        .v-leave-to {
            opacity: 0;
        }

        .v-leave-active {
            transition: 1s all;
        }
    </style>
    <div id='app'>
        <input type="text" v-model="name">
        <input type="submit" value="添加" @click.prevent="add()">
        <!-- v-for的渲染机制:在渲染元素的时候首先会在缓存中查找有没有需要渲染的元素
            如果没有会在缓存中创建一个新的并将其渲染出来,如果有那么会直接复用原有的
            注意:在Vue中只要数据一改变就会重新渲染一次 -->
        <transition-group appear tag="ul">
            <li v-for="(person,index) in persons" :key="person.id" @click="del(index)"><input type="checkbox"
                checked>{{index}}---{{person.name}}
            </li>
        </transition-group>
    </div>
    <script>
        let vue = new Vue({
            el: '#app',
            data: {
                persons: [{ name: 'zs', id: 2 }, { name: 'ls', id: 1 }, { name: 'ww', id: 0 }],
                name: "",
            },
            methods: {
                add() {
                    let newId = this.persons[0].id;
                    this.persons.unshift({
                        name: this.name,
                        id: newId + 1
                    });
                    this.name = "";
                },
                del(index) {
                    this.persons.splice(index, 1);
                }
            }
        });
    </script>

  image.png

总结:


好了Vue的动画在这里就介绍的差不多了,当然还有其他细节,比如我们可以自定义动画的类名等,就不细细介绍了,这里总结一下上面说的三个Vue动画方法。

首先是添加类名,我们要记得像v-enter,v-enter-to,v-enter-active对应的类名添加时间,如果我们想要在初始渲染时就有动画效果我们给transition添加appera属性便好,同时我们还可以通过name修改类名前缀。

如果我们需要使用钩子函数添加动画,要记得有enter,before-enter,after-enter三个钩子函数,我们对应的方法中默认会接收当前添加动画的元素el。我们需要在transition中添加:css="false"来让CSS类名过渡不再起作用,我们还需要在enter或者leave中执行done()方法,如果是appear状态还需要让done()延时执行。

最后就是transition-group,默认是渲染成span标签,我们可以通过tag="tagName"设置渲染的标签名。


好了说到这里我想大家对Vue动画应该有了更深入的理解了(其实看文档就知道了),写这篇文章的目的还是为了面试,也是补足一下自己对于前端动画这方面的理解。