requestAnimationFrame 和 setTimeout 的区别

requestAnimationFrame 就是该API能以浏览器的显示频率来作为其动画动作的频率,比如浏览器每10ms刷新一次,动画回调也每10ms调用一次,这样就不会存在过度绘制的问题,动画不会掉帧,自然流畅。

基本语法

window.requestAnimationFrame(callback);

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

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

用法其实跟 setTimeout 完全一致,只不过当前的时间间隔是跟着系统的绘制频率走,是固定的

优点

使得动画更加流畅,防止动画失帧 requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率 资源节能(Cpu、内存等) 在隐藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流,这当然就意味着更少的 CPU、GPU 和内存使用量 requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了 CPU 开销

warning

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

当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数(即你的回调函数)。回调函数执行次数通常是每秒60次,但在大多数遵循W3C建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame() 运行在后台标签页或者隐藏的 里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命。

回调函数会被传入DOMHighResTimeStamp参数,DOMHighResTimeStamp指示当前被 requestAnimationFrame() 排序的回调函数被触发的时间。在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。该时间戳是一个十进制数,单位毫秒,最小精度为1ms(1000μs)。

requestAnimationFrame 和setInterval、setTimeout的联系

因为 setTimeout 和 setInterval 是异步 api,必须需要等同步任务执行,还需要等待微任务完成以后,然后才会去执行当前这个回调函数。 这里会存在一个问题,没有办法去精准地把时间定位到,哪怕你写成 16,它也没有办法,让时间精准定位到 16。时间间隔没有办法保证。 与 setTimeout 相比,requestAnimationFrame 最大的优势是 由系统来决定回调函数的执行时机。 如果系统绘制率是 60Hz,那么回调函数就每16.7ms 被执行一次,如果绘制频率是75Hz,那么这个间隔时间就变成了 1000/75=13.3ms,也就是说它的时间间隔,是跟着系统的绘制频率走。

浏览器渲染有个渲染时机(Rendering opportunity)的问题,也就是浏览器会根据当前的浏览上下文判断是否进行渲染,它会尽量高效,只有必要的时候才进行渲染,如果没有界面的改变,就不会渲染。按照规范里说的一样,因为考虑到硬件的刷新频率限制、页面性能以及页面是否存在后台等等因素,有可能执行完 setTimeout 这个 task 之后,发现还没到渲染时机,所以 setTimeout 回调了几次之后才进行渲染

requestAnimationFrame的应用场景

大数据渲染

在大数据渲染过程中,比如表格的渲染,如果不进行一些性能策略处理,就会出现 UI 冻结现象,用户体验极差。有个场景,将后台返回的十万条记录插入到表格中,如果一次性在循环中生成 DOM 元素,会导致页面卡顿5s左右。这时候我们就可以用 requestAnimationFrame 进行分步渲染,确定最好的时间间隔,使得页面加载过程中很流畅。

var total = 100000;

var size = 100;

var count = total / size;

var done = 0;

var ul = document.getElementById('list');

function addItems() {

var li = null;

var fg = document.createDocumentFragment();

for (var i = 0; i < size; i++) {

li = document.createElement('li');

li.innerText = 'item ' + (done * size + i);

fg.appendChild(li);

}

ul.appendChild(fg);

done++;

if (done < count) {

requestAnimationFrame(addItems);

}

};

requestAnimationFrame(addItems);

实现动画

css3实现使得性能和流畅度都得到了很大的提升,但同时局限性也挺大比如不是所有的属性都能参与动画,动画过程不能完全控制,动画缓动效果太小等等。 刚好相反的是setTimeout和setInterval能达成更多的可控性质的自有帧动画,但是由于刷新时间和定时器时间不同会出现掉帧现象,定时器时间设的越短掉帧时间越严重,而且性能牺牲很严重 然而 requestAnimationFrame 的出现让我们有了除了这两种我们常用的方案之外的另一种更优的选择

总结

setTimeout 的弊端:

因为setTimeout受事件队列的影响,回调的执行时机要在任务栈清空后才会压栈,这就导致setTimeout回调执行时机往往会比设置的间隔时间要晚,而requestAnimationFrame的回调是由浏览器调度的,在绘制帧前会自动执行,不存在精度问题。(因为受event loop影响,即使定时器设置了16.67ms的间隔,但是因为定时器回调是宏任务,同步任务栈清空之前,宏任务不会被压栈,当主线程执行同步任务时间过长,定时器的回调执行时间也会被延后,这就导致了回调执行的间隔大于16.67ms)不同设备的屏幕绘制频率可能会不同,比如120hz的屏幕绘制时间就是8.4ms, 而 setTimeout 只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。动画有时会出现抖动:因为setTimeout操作的dom变化,会在浏览器下一次重绘之前执行,否则只会停留在内存中。而定时器回调因为无法保证跟浏览器重绘时间重合,会导致某一帧没有绘制,直接绘制下一帧,出现跳跃的情况,所以才会抖动。

setInterval和setTimout不够精确的原因是内在机制决定。当计时结束不意味着回调函数会立即执行,而是将回调函数放到回调队列末尾,导致实际执行时间总是大于预定的时间 requestAnimationFrame 采用 浏览器时间间隔 ,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,消耗性能;也不会因为间隔时间太长,使用动画卡顿不流畅

参考资料: https://juejin.cn/post/7062178363800027173#heading-7

查看原文