阅读 281
放弃定时器,使用更高效的requestAnimationFrame来做动画

放弃定时器,使用更高效的requestAnimationFrame来做动画

动画原理: 计算机每16.7ms刷新一次,由于人眼的视觉停留,所以看起来是流畅的移动。

屏幕刷新频率: 屏幕每秒出现图像的次数,大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会有提升。因此,最平滑动画的最佳循环间隔是1000ms/60,约等于16.7ms

实现动画效果的方法比较多,Javascript 中可以通过定时器来实现,css3 可以使用 transitionanimation 来实现,html5 中的 canvas 也可以实现。除此之外,html5 还提供一个专门用于请求动画的API,那就是 requestAnimationFrame,顾名思义就是请求动画帧。

setTimeout和setInterval的问题是,它们都不精确。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。

requestAnimationFrame采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,改善视觉效果。

语法

window.requestAnimationFrame(callback);
复制代码
  • requestAnimationFrame的用法与settimeout很相似,只是不需要设置时间间隔而已

  • 若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用requestAnimationFrame()

参数

callback

  • 下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数)。该回调函数会被传入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻。

  • 在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。该时间戳是一个十进制数,单位毫秒,最小精度为1ms(1000μs)。请确保总是使用第一个参数(或其它获得当前时间的方法)计算每次调用之间的时间间隔,否则动画在高刷新率的屏幕中会运行得更快。

返回值

一个 long 整数,请求 ID ,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给 window.cancelAnimationFrame() 以取消回调函数。

范例

<div class="progress-wrap">
  <div class="progress" id="progress"></div>
</div>
<div class="controls">
    <button id="start">start</button>
    <button id="stop">stop</button>
    <button id="reset">reset</button>
</div>
复制代码
* {
    margin: 0;
    padding: 0;
}
.progress-wrap {
    background: #eee;
    height: 50px;
}
.progress-wrap .progress {
    background: green;
    width: 0;
    height: 100%;
}
复制代码
const domProgress = document.querySelector('#progress')
let progressStep = 0
let timer = ''

function progress(timeStamp) {
    if(progressStep <= 100) {
        domProgress.style.width = `${progressStep}%`
        progressStep++
        timer = window.requestAnimationFrame(progress)
    }
}

document.querySelector('#start').onclick = () => {
    window.cancelAnimationFrame(timer)
    timer = window.requestAnimationFrame(progress)
}
document.querySelector('#stop').onclick = () => {
     window.cancelAnimationFrame(timer)
}
document.querySelector('#reset').onclick = () => {
    window.cancelAnimationFrame(timer)
    domProgress.style.width = 0
    progressStep = 0
    timer = ''
}
复制代码

setTimeout实现的弊端

通过设定间隔时间来不断改变图像位置,达到动画效果。但是容易出现卡顿、抖动的现象。原因是:

  1. settimeout任务被放入异步队列,只有当主线程任务执行完后才会执行队列中的任务,因此实际执行时间总是比设定时间要晚;
  2. settimeout的固定时间间隔不一定与屏幕刷新时间相同,会引起丢帧。

requestAnimationFrame优势

由系统决定回调函数的执行时机。60Hz的刷新频率,那么每次刷新的间隔中会执行一次回调函数,不会引起丢帧,不会卡顿

CPU节能: 使用setTimeout实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费CPU资源。

而requestAnimationFrame则完全不同。当页面运行在后台标签页或者隐藏的<iframe>,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的requestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销,提高性能和电池寿命。

函数节流: 在高频率事件(resize,scroll等)中,为了防止在一个刷新间隔内发生多次函数执行,使用requestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。

集中重绘或回流: requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率

优雅降级

由于兼容性问题,需要降级对接口进行封装,优先使用高级特性,再根据浏览器不同情况进行回退,直到只能使用setTimeout。

简单处理

if (!window.requestAnimationFrame) {
    requestAnimationFrame = function(fn) {
        setTimeout(fn, 16.7);
    };    
}
复制代码

进一步处理

if(!window.requestAnimationFrame){
    var lastTime = 0;
    window.requestAnimationFrame = function(callback){
        var currTime = new Date().getTime();
        var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
        var id  = window.setTimeout(function(){
            callback(currTime + timeToCall);
        }, timeToCall);
        lastTime = currTime + timeToCall;
        return id;
    }
}

if (!window.cancelAnimationFrame) {
    window.cancelAnimationFrame = function(id) {
        clearTimeout(id);
    };
}
复制代码
文章分类
前端
文章标签