window.requestAnimationFrame强大的前端动画神器

1,593 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

今天介绍一个功能强大的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函数
	}
	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函数
	}
	move();
}

animationTest()

好啦,如果你有在控制台测试,你就能看到,多了一个控制动画的按钮,现在你可以自由的控制动画的开始和停止了

其他场景

刚才我们举了一个简单动画的的例子,那么我们在其他场景会不会用到这个api呢,当然是用的到的, 尤其是做音视频,或者对应用性能有一定要求的项目中,requestAnimationFrame方法基本上是绕不开的。 比如在移动端等端上设备模拟亮度,一般会有一个由亮到暗,或者由暗到亮的过渡,或者页面平滑滚动,再或者需要一个基于画面刷新率的观测器都可以用的到。这些和动画大同小异。但是这里再介绍一个非常常见的场景,鼠标事件

mousemove

我们知道,mousemove的触发频率很高,很多时候,我们不需要这么高的触发频率,常常为了优化性能,我们会写一个截流函数,来降低它触发的频率,如果你仅仅是为了让画面看起来更流畅,对频率没有特殊的需求。那么你可以直接使用requestAnimationFrame,这样mousemove的触发频率就会与画面刷新率保持一致,既保证了画面的流畅度,又降低了mousemove的触发次数,纯天然绿色截流,你值得拥有

function mousemoveTest(){
	document.addEventListener("mousemove", move);
	
	function move(){
		requestAnimationFrame(() => {
			// todo 高性能消耗的代码
			console.log("move 函数执行了")
		})
	}
}
mousemoveTest()

这一步很难看到效果,如果你在move函数中执行了一段,性能消费很高的代码,比如拖拽某些dom的时候,需要大量的计算,造成画面卡顿。这个时候,用这种方式,就能看到明显的效果了

总结

这里就不在举更多的例子了,简单总结一下,requestAnimationFrame(callback), 其中callback就是你想要执行的函数,将callback作为参数传递给requestAnimationFrame的时候,就表示你希望在浏览器绘制下一帧的时候,去调用这个函数。当你需要多次触发被requestAnimationFrame包裹callback的时候,在浏览器绘制一帧的的过程中,不论你触发多少次,它都仅会执行一次。正因为如此,所以可以当作一个16ms一次的截流函数来使用。也是因为如此,我们才可以在一个看似死循环的递归中使用它。

当然,requestAnimationFrame也是它存在的问题,比如他只会在当前页面激活时被触发,也就是说,不论你切了浏览器tab标签,还是切换到了其他的程序,只要当前的浏览器页面没有被激活,requestAnimationFrame是不会触发的。在一些有严格要求的动画中,会导致丢帧的情况。解决这种丢帧问题的方法,一般会采用监听visibilitychange事件,当用户离开当前页面时,我们记录离开的时间戳,当用户再次回到页面时,我们利用记录的时间戳计算用户离开页面的时间,并计算出在这段时间内丢失的动画状态,同时将动画状态进行补偿,这样就可以解决丢帧的问题了。