实现tweenlite数字滚动方法

1,492 阅读4分钟

前段时间在项目中遇到一个需求,需要展示数字滚动的特效,正好之前阅读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。

通常在页面添加动画的话有几种方式:

  1. 设置 left,top 会触发 reflow ,所以基本不会使用这种方法。

  2. 设置 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 操作删除复合图层。

  3. 初次使用 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 上和动画相关的库非常丰富,上述的代码也不够完善(参数类型检测,兼容,加入缓动算法等等),但有时候动手实现一下并不是为了重复造轮子,更多的是用有限的精力扩展自身的思维方式。

参考文献:

requestAnimationFrame详解以及无线页面优化

浏览器渲染流程&Composite(渲染层合并)简单总结