前端动画实现方式总结 - childlabor/blog GitHub Wiki

前言

通常在前端中,实现动画的方案主要有6种:

javascript直接实现

requestAnimationFrame(api)

CSS3 transition(过渡)

CSS3 animation(关键帧)

SVG(可伸缩矢量图形) & Canvas(位图)

js

其主要思想是通过setInterval或setTimeout方法的回调函数来持续调 用改变某个元素的CSS样式以达到元素样式变化的效果。

let elem = document.getElementById('demo');
let left = 0;
let timer = setInterval(function() {
    if(left < window.innerWidth-200) {
        elem.style.marginLeft = `${left}px`;
        left ++;
    }else {
        clearInterval(timer);
    }
}, 100);

由于JavaScript是单线程的,所以定时器的实现是在执行栈中的同步任务完成后再执行定时器的回调的,假如当前同步任务执行时间大于定时器设置的延迟时间,那么定时器就不是那么可靠了,动画是由浏览器按照一定的频率一帧一帧的绘制的,浏览器压根就无法保证每一帧渲染的时间间隔,实际的动画效果可能有偏差。

requestAnimationFrame

HTML5专门用于请求动画的api,绘制时间间隔是1000/60ms(每秒60帧)。使用requestAnimationFrame,我们不用去操心每一帧动画渲染的时间间隔问题,只需要设计好每帧该做什么。

不兼容 IE9及以下浏览器,可以使用定时器做兼容。反正IE嘛,要什么自行车。

如果要取消动画,可以将其返回值传给 window.cancelAnimationFrame() 以取消回调函数。

  let left4 = 0;
  let reqA = null;
  elem4.onclick = function() {
    reqA = requestAnimationFrame(animate);
  };
  // 取消
  btn.onclick = function() {
    window.cancelAnimationFrame(reqA);
  };

  function animate() {
    elem4.style.marginLeft = `${left4 *200/60}px`;
    left4 ++;
    if(left4 < 181) {
      reqA = requestAnimationFrame(animate);
    }
  }

优势

  1. 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随显示器的刷新频率;
  2. 在隐藏或不可见的元素中,将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量;

CSS transition

transition CSS 属性是 transition-propertytransition-durationtransition-timing-functiontransition-delay 的一个简写属性。过渡可以为一个元素在不同状态之间切换的时候定义不同的过渡效果。比如在不同的伪元素之间切换,像是 :hover,:active 或者通过 JavaScript 实现的状态变化。transition属性可以被指定为一个或多个 CSS 属性的过渡效果,多个属性之间用逗号进行分隔。

.demo {
    width: 20px;
    height: 20px;
}
.demo:hover {
    width: 50px;
    height: 50px;
    transition: width 1s ease, height 1s linear 1s;
}
/* 顺带一提,transition属性写在元素不同状态下的效果是不一样的。 */
.demo {
    width: 20px;
    height: 20px;
    transition: width 1s ease, height 1s linear 1s;
}
.demo:hover {
    width: 50px;
    height: 50px;
}
监听transitions的启动和完成

可以配合js实现一些效果

el.addEventListener("transitionstart", func1);
el.addEventListener("transitionrun", func2);
el.addEventListener("transitionend", func3);

CSS animation

通过对==关键帧==和==循环次数==的控制,页面标签元素会根据设定好的样式改变进行平滑过渡。而且关键帧状态的控制是通过百分比来控制的,更加的精确。

基本属性和transition基本一致,相比transition的一次性动画,animation是可以控制播放次数的。

#d3 {
  animation: toRight 3s linear 0s 1 forwards;
}

@keyframes toRight {
  from {
    margin-left: 0;
  }
  to {
    margin-left: 600px;
  }
}

可以通过animation-play-state(running|paused|...)属性控制启动和暂停。

SVG & canvas

SVG:可缩放==矢量==图形,用来定义用于网络的基于矢量的图形,其使用 XML 格式定义图像,很适合修改和扩展。

canvas:基于像素的==位图==,作为页面图形绘制的容器(画布),可用于通过使用js中的脚本来绘制图形,因为跟js结合更紧密,所以一些逻辑操作要比css3方便太多,比如用canvas逐帧写动画和游戏。

实际应用中,复杂的动画,我们一般选择

  1. 设计师切gif
  2. png序列帧
  3. svg & canvas

对于使用图片实现方式,都会有文件较大、容易有锯齿的问题,如果图片无法满足要求,我们就可以选择SVG。因为数据结构的复杂性,通常我们会使用lottie-web插件来帮助渲染。Lottie可以解析使用AE制作的动画(需要用bodymovin导出为json格式)。

// 使用方式很简单
import lottie from 'lottie-web'

lottie.loadAnimation({
  container: element, // the dom element that will contain the animation
  renderer: 'svg',
  loop: true,
  autoplay: true,
  path: 'data.json' // the path to the animation json
});

对比及性能

几个概念

回流:当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流

重绘:当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘

JavaScript操作DOM,CSS3 Animation或Transition等实现动画通常会导致页面频繁性回流重绘,消耗性能。我们可以在控制台performance查看页面的回流重绘。

这里给大家推荐一个网站,里面详细列出了哪些CSS属性在不同的渲染引擎中是否会触发重排或重绘https://csstriggers.com/

JS动画

缺点:JavaScript在浏览器的主线程中运行,而其中还有很多其他需要运行的JavaScript、样式计算、布局、绘制等对其干扰。这也就导致了线程可能出现阻塞,从而造成丢帧的情况。

优点:JavaScript的动画与CSS预先定义好的动画不同,可以在其动画过程中对其进行控制:开始、暂停、回放、中止、取消都是可以做到的。而且一些动画效果,比如视差滚动效果,只有JavaScript能够完成

CSS动画

缺点:缺乏强大的控制能力。而且很难以有意义的方式结合到一起,使得动画变得复杂且易于出问题。

优点:浏览器可以对动画进行优化。它必要时可以创建图层,开启硬件加速。详细可参考CSS动画原理及硬件加速

相关库/框架

Animate.css :纯css的动画库

animate.js : 轻量的动画库

lottie-web :SVG渲染插件

pixi.js : WebGL的2D渲染

three.js : WebGL的3D渲染

总结

篇幅原因,以上的点并没有太过深究,只是点到为止。实际项目开发中,我们通常会选择多个方式以组合的形式来实现需求,而优化方案也不能一概而论,还是应该根据实际需求来进行。

⚠️ **GitHub.com Fallback** ⚠️