前段时间在项目中遇到一个需求,需要展示数字滚动的特效,正好之前阅读vue官方文档时被安利了gsap这个插件,直接拉下来,一行代码解决问题。事后想想这种程度的需求完全可以自己实现,这里用到了 requestAnimationFrame 方法。
在此之前先大概熟悉一下浏览器渲染过程:
1.五个步骤都消耗了时间
当我们在js中改变了某个DOM元素的layout时,那么浏览器就会检查页面中的哪些元素需要重新布局,然后对页面激发一个reflow过程以完成页面的重新布局。被reflow的元素,接下来就一定会再次经过Paint和Composite这两个过程,以渲染出最新的页面。
2.跳过layout这一步
当我们只修改了一个DOM元素的paintonly属性的时候,比如background-image/color/box-shadow等。这个时候不会触发layout,浏览器在完成样式的计算之后就会跳过layout的过程,就只Paint和Composite了。
3.跳过layout和paint这两步
如果你修改一个非样式且非绘制的CSS属性,那么浏览器会在完成样式计算之后,跳过布局和绘制的过程,直接Composite。
通常在页面添加动画的话有几种方式:
-
设置 left,top 会触发 reflow ,所以基本不会使用这种方法。
-
设置 transform :
这里涉及到合成层的概念。页面的绘制并不是在单层的画面里完成的,满足一些条件的渲染层被称为合成层。合成层有自己的渲染上下文,并且交由 GPU 处理,比 CPU 要快。当页面需要重绘时,合成层的元素只会重绘自己层内的元素,而非整个页面。
那么将普通渲染层提升至合成层的操作有:- 3D transforms: translate3d, translateZ 等;
- video, canvas, iframe 等元素;
- 通过 Element.animate() 实现的 opacity 动画转换;
- 通过 СSS 动画实现的 opacity 动画转换;
- position: fixed;
- will-change;
- filter;
其中需要说明的是,3D 和 2D transform 的区别就在于,浏览器在页面渲染前为3D动画创建独立的复合图层,而在运行期间为2D动画创建。动画开始时,生成新的复合图层并加载为GPU的纹理用于初始化 repaint。然后由GPU的复合器操纵整个动画的执行。最后当动画结束时,再次执行 repaint 操作删除复合图层。
-
初次使用 requestAnimationFrame 方法是在用 canvas 写 loading 动画的时候。requestAnimationFrame 的优势,在于充分利用显示器的刷新机制,比较节省系统资源。显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame 的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。
接下来上代码,以sin函数为变化速率为例:
使用:new Tween().to(this.$data , .8 , {num:123})//需要修改的对象,持续时间,修改后的属性
实现:
export default class Tween {
constructor() {
this.base = {}; // 存放对象中的初始值
this.distance = {}; // 存放对象中属性的初始值与最终值的差值
this.rate = 0; //存放差值变化的速率,从0-1;
this.count = 0; // 存放持续时间内的帧数,依次增加
this.frame = null; // requestAnimationFrame的返回值
this.stop = false; //停止的标志位
}
to(dataPool,seconds,targetData) {
this.dataPool = dataPool;
this.seconds = seconds;
this.targetData = targetData;
this.getDistance()
}
//存放对象中的初始值以及初始值与最终值的差值
getDistance() {
Object.keys(this.targetData).forEach(k => {
this.base[k] = this.dataPool[k];
this.distance[k] = this.targetData[k] - this.dataPool[k];
})
this.transform()
}
/****
* 设定一秒执行60次,在 this.seconds * 60 的次数里,
* 使得差值依次从 差值 * Math.sin(0) 至 差值 * Math.sin(Math.PI/2) 变化。
****/
transform() {
this.rate = 0.5 * Math.PI * this.count / this.seconds / 60;
this.count ++ ;
Object.keys(this.targetData).forEach(k => {
this.dataPool[k] = this.base[k] + this.distance[k] * Math.sin(this.rate);
if(this.count >= this.seconds * 60) {
this.dataPool[k] = this.targetData[k]
this.stop = true;
}
})
this.frame = window.requestAnimationFrame(this.transform.bind(this));
if(this.stop) {
window.cancelAnimationFrame(this.frame)
}
}
}
结论:数字可以如期变化,并且在其他需要数值改变来实现动画的场景中仍可以使用,比如box-shadow,rgba ……
结束语: 当然目前 github 上和动画相关的库非常丰富,上述的代码也不够完善(参数类型检测,兼容,加入缓动算法等等),但有时候动手实现一下并不是为了重复造轮子,更多的是用有限的精力扩展自身的思维方式。