今天介绍一个功能强大的api—window.requestAnimationFrame,它既可以实现如丝般顺滑的动画,又能充当性能优化的利器,还能代替setTimeout,setInterval等定时器。自从学会了requestAnimationFrame,我已经不会拼写setInterval啦…
背景
动画的实现与浏览器显示
在讲具体功能和api使用方法之前,我们先来大体聊一下api的背景和原理。
在各类影视节目横行的今天,大家应该都对电影或是动画的实现有一定了解,最开始的动画是工作人员,一张图一张图画出来的,然后通过快速的切换图片,使静态的画面“运动起来”,人们认为,人类的肉眼所能分辨的频率的极值约为50ms/次,也就是说,只要画面在50ms内快速切换,人类就几乎发现不了画面切换代码的顿挫感,观测到的画面是一种顺滑的流畅的图像。这也就是原始动画片的制作原理。
浏览器的画面,和动画片类似,也是浏览器按照一定频率一帧一帧绘制出来的,一般情况下浏览器的刷新率为16ms绘制一帧。也就是说,这个频率的绘制,会让人完全感受不到画面的闪烁,让图像真正的动起来。
浏览器绘制每一帧,都会按照以下过程进行:(省略一些不太相关的过程) 1、开始新的一帧率 2、处理输入事件 3、执行requestAnimationFrame 4、解析html 5、计算样式 6、更新图层树 7、发送帧
通过这个过程不难发现,每当浏览器开始绘制新的一帧画面的时候,都会去执行一下requestAnimationFrame,那如果我们讲动画相关的代码,通过requestAnimationFrame来执行,是不是就可以做到和浏览器本身画面一样顺滑了呢?
window.requestAnimationFrame
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
举个例子 如果你希望得到一个向右侧移动的方块,那么你可以这样
function animationTest(){
const div = document.createElement("div");
div.style.width = '100px'
div.style.height = '100px'
div.style.position = 'absolute'
div.style.top = '0px';
div.style.left = '0px';
div.style.backgroundColor = '#f00'
div.style.zIndex = '999999'
document.body.appendChild(div);
let distance = 0;
function move(){
distance++
console.log(distance)
div.style.left = distance + 'px'
requestAnimationFrame(move);
}
move();
}
animationTest()
这段代码你可以打开浏览器,F12打开控制台,复制进去测试。 你应该可以看到一个红色的方块,在浏览器缓慢的向右侧移动。 同时相信你用不了多久,就会发现,这个动画怎么停下来呢?
window.cancelAnimationFrame
取消一个先前通过调用window.requestAnimationFrame()方法添加到计划中的动画帧请求.
想要通知浏览器,下一帧的时候,不要继续执行requestAnimationFrame的回调啦,那么你需要像使用setTimeout一样,存一下requestAnimationFrame返回的id,并传入到cancelAnimationFrame
比如这样
function animationTest(){
let animationId;
const button = document.createElement("button");
button.innerHTML = "停止动画"
button.style.position = 'absolute'
button.style.top = '150px';
button.style.left = '0px';
button.style.zIndex = '999999'
button.onclick = () => {
if(!!animationId){
window.cancelAnimationFrame(animationId);
button.innerHTML = "开始动画"
animationId = void 0;
}else{
move()
button.innerHTML = "停止动画"
}
}
const div = document.createElement("div");
div.style.width = '100px'
div.style.height = '100px'
div.style.position = 'absolute'
div.style.top = '0px';
div.style.left = '0px';
div.style.backgroundColor = '#f00'
div.style.zIndex = '999999'
document.body.appendChild(div);
document.body.appendChild(button);
let distance = 0;
function move(){
distance++
console.log(distance)
div.style.left = distance + 'px'
animationId = requestAnimationFrame(move);
}
move();
}
animationTest()
好啦,如果你有在控制台测试,你就能看到,多了一个控制动画的按钮,现在你可以自由的控制动画的开始和停止了
其他场景
刚才我们举了一个简单动画的的例子,那么我们在其他场景会不会用到这个api呢,当然是用的到的, 尤其是做音视频,或者对应用性能有一定要求的项目中,requestAnimationFrame方法基本上是绕不开的。 比如在移动端等端上设备模拟亮度,一般会有一个由亮到暗,或者由暗到亮的过渡,或者页面平滑滚动,再或者需要一个基于画面刷新率的观测器都可以用的到。这些和动画大同小异。但是这里再介绍一个非常常见的场景,鼠标事件
mousemove
我们知道,mousemove的触发频率很高,很多时候,我们不需要这么高的触发频率,常常为了优化性能,我们会写一个截流函数,来降低它触发的频率,如果你仅仅是为了让画面看起来更流畅,对频率没有特殊的需求。那么你可以直接使用requestAnimationFrame,这样mousemove的触发频率就会与画面刷新率保持一致,既保证了画面的流畅度,又降低了mousemove的触发次数,纯天然绿色截流,你值得拥有
function mousemoveTest(){
document.addEventListener("mousemove", move);
function move(){
requestAnimationFrame(() => {
console.log("move 函数执行了")
})
}
}
mousemoveTest()
这一步很难看到效果,如果你在move函数中执行了一段,性能消费很高的代码,比如拖拽某些dom的时候,需要大量的计算,造成画面卡顿。这个时候,用这种方式,就能看到明显的效果了
总结
这里就不在举更多的例子了,简单总结一下,requestAnimationFrame(callback), 其中callback就是你想要执行的函数,将callback作为参数传递给requestAnimationFrame的时候,就表示你希望在浏览器绘制下一帧的时候,去调用这个函数。当你需要多次触发被requestAnimationFrame包裹callback的时候,在浏览器绘制一帧的的过程中,不论你触发多少次,它都仅会执行一次。正因为如此,所以可以当作一个16ms一次的截流函数来使用。也是因为如此,我们才可以在一个看似死循环的递归中使用它。
当然,requestAnimationFrame也是它存在的问题,比如他只会在当前页面激活时被触发,也就是说,不论你切了浏览器tab标签,还是切换到了其他的程序,只要当前的浏览器页面没有被激活,requestAnimationFrame是不会触发的。在一些有严格要求的动画中,会导致丢帧的情况。解决这种丢帧问题的方法,一般会采用监听visibilitychange事件,当用户离开当前页面时,我们记录离开的时间戳,当用户再次回到页面时,我们利用记录的时间戳计算用户离开页面的时间,并计算出在这段时间内丢失的动画状态,同时将动画状态进行补偿,这样就可以解决丢帧的问题了。
|