requestAnimationFrame 与延时执行函数 setTimeout 类似,但不用设置时间,即可将操作延迟至下一次网页重绘时执行。
requestAnimationFrame 的用法
延迟到下一帧执行
requestAnimationFrame(回调函数)
功能:将回调函数,延迟到下一帧(下一次网页重绘)前执行。
每一帧都执行
在回调函数内再次调用 requestAnimationFrame()
requestAnimationFrame(function update() { console.log("执行了更新!"); // requestAnimationFrame 的回调函数默认只会被调用一次,如果希望每帧都执行,则每帧都需要调用 requestAnimationFrame(update); });
取消执行
requestAnimationFrame(回调函数)
会返回一个整数(定时器的编号),将其传给cancelAnimationFrame(定时器的编号)
可取消回调函数的执行,如:
// 定义定时器 let timer1 = requestAnimationFrame(回调函数) // 取消定时器 cancelAnimationFrame(timer1)
requestAnimationFrame 的常见应用场景
提升 web 性能
使用 requestAnimationFrame 替换 setTimeout 和 setInterva 可提升web性能。
setTimeout 和 setInterva 实现动画的缺点
- 时间不精确,通常实际执行时间都要比其设定的时间晚一些。
- 实现的动画在某些低端机上会出现卡顿、抖动的现象。(不同设备的屏幕刷新频率不同,而 setTimeout 和 setInterva 只能设置一个固定的时间间隔,这个时间和屏幕的刷新时间不同时,就会出现掉帧,而导致页面卡顿、抖动)
requestAnimationFrame 实现动画的优势
requestAnimationFrame 采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。
全局监听浏览器滚动事件
// 全局监听浏览器滚动事件 window.onscroll = function () { requestAnimationFrame(function scroll() { console.log("页面滚动了"); }); };
平滑滚动
<button onclick="scrollToTop()">回到顶部</button>
function scrollToTop() { const c = document.documentElement.scrollTop || document.body.scrollTop; if (c > 0) { window.requestAnimationFrame(scrollToTop); window.scrollTo(0, c - c / 8); } }
大量数据的渲染
比如十万条数据的渲染,使用 requestAnimationFrame 分页加载。
//需要插入的容器 let ul = document.getElementById('container') // 插入十万条数据 let total = 100000 // 一次插入 20 条 let once = 20 //总页数 let page = total / once //每条记录的索引 let index = 0 //循环加载数据 function loop(curTotal, curIndex) { if (curTotal <= 0) { return false } //每页多少条 let pageCount = Math.min(curTotal, once) window.requestAnimationFrame(function () { for (let i = 0; i < pageCount; i++) { let li = document.createElement('li') li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total) ul.appendChild(li) } loop(curTotal - pageCount, curIndex + pageCount) }) } loop(total, index)
使用 setTimeout 实现的写法如下:
//需要插入的容器 let ul = document.getElementById('container') // 插入十万条数据 let total = 100000 // 一次插入 20 条 let once = 20 //总页数 let page = total / once //每条记录的索引 let index = 0 //循环加载数据 function loop(curTotal, curIndex) { if (curTotal <= 0) { return false } //每页多少条 let pageCount = Math.min(curTotal, once) setTimeout(() => { for (let i = 0; i < pageCount; i++) { let li = document.createElement('li') li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total) ul.appendChild(li) } loop(curTotal - pageCount, curIndex + pageCount) }, 0) } loop(total, index)
监控页面的卡顿
比如连续出现 3 个低于 20 的 FPS 即可认为网页存在卡顿。
var lastTime = performance.now() var frame = 0 var lastFameTime = performance.now() var loop = function (time) { var now = performance.now() var fs = now - lastFameTime lastFameTime = now var fps = Math.round(1000 / fs) frame++ if (now > 1000 + lastTime) { var fps = Math.round((frame * 1000) / (now - lastTime)) frame = 0 lastTime = now } window.requestAnimationFrame(loop) }
requestAnimationFrame 的优点
- requestAnimationFrame 会把每一帧中所有的DOM操作集中起来,在一次重绘/回流中完成。
重绘:当某个元素颜色样式发生更改时(如背景颜色、文字颜色),页面也需要更新,浏览器需要重新绘制元素,称为重绘(repaint)。
回流:当页面上的某一个元素的大小或者位置发生更改时,都会影响到与它相邻元素的状况,甚至整个页面的元素状态(位置、元素大小)都需要重新计算和更新。这种操作称为回流(reflow)或者布局(layout)。一个页面至少会有一次回流,就是在页面初始化时。
requestAnimationFrame 重绘/回流的时间间隔紧紧跟随浏览器的刷新频率。充分利用了显示器的刷新机制,比较节省系统资源(显示器有固定的刷新频率60Hz/75Hz,即每秒最多重绘60次或75次,requestAnimationFrame的执行紧跟着系统的绘制频率走,便能保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题)。
对隐藏或不可见的元素,requestAnimationFrame不会进行重绘/回流,这意味着更少的CPU、GPU和内存使用量。
当页面被隐藏或最小化时,requestAnimationFrame会停止渲染(setTimeout 和setInterval 仍然在后台执行动画任务),当页面被激活时,动画会从上次停留的地方继续执行,有效节省了CPU、GPU和电力开销。
requestAnimationFrame是由浏览器专门为动画提供的API,在运行时浏览器会自动优化方法的调用
requestAnimationFrame 的缺点
- requestAnimationFrame 存在兼容性问题,在不支持的浏览器(如 IE9),需添加兼容代码
- requestAnimationFrame 使用的主线程,若主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。
兼容写法
对不支持 requestAnimationFrame 的浏览器(如 IE9),需在代码前添加以下兼容代码:
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; } }