requestAnimationFrame是什么
window.requestAnimationFrame,根据W3C标准的解释,我们可以通过调用它来告诉浏览器,需要执行一个动画,并且要求浏览器在 下次重绘之前 调用指定的回调函数更新动画。所以该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
下次重绘之前 是啥意思
怎么去理解 下次重绘之前 呢,先来看下这两行代码
document.body.appendChild(el)
el.style.display = 'none'
这种代码总会让我感觉有点别扭,我会考虑el这个元素会不会“一闪而过”,
通常我都习惯把这两行代码倒过来写。这么到底写会不会出问题呢?答案是不会的。
Js操作dom到浏览器渲染的过程如下:

JavaScript:JavaScript实现动画效果,DOM元素操作等。
Style(css样式):收集DOM元素对应的CSS。
Layout(布局):计算DOM元素在最终屏幕上显示的大小和位置。由于web页面的元素布局是相对的,所以其中任意一个元素的位置发生变化,都会联动的引起其他元素发生变化,这个过程叫reflow(回流)。
Paint(绘制):在多个层上绘制DOM元素的的文字、颜色、图像、边框和阴影等。
Composite(渲染层合并):按照合理的顺序合并图层然后显示到屏幕上。
大部分从Style 到 Composite 这个过程是跟屏幕的刷新频率有关的,即如果屏幕刷新频率是60Hz的话,Style 到 Composite 这个过程的循环间隔是(1000/60)ms。所以若干行js代码如果运行在同一帧内,下一帧反应到页面上的状态就是dom最新的样式。
那么demo1的代码对应的浏览器渲染过程就像如下图:
想象每个长长的矩形都是一帧,每一帧的开头会执行Style -> Composite这个过程,如果task1、2、3操作了同一个dom的同一个属性,那么最终在执行Style的时候只有上一帧的task3会生效。
document.body.appendChild(el)
setTimeout(() => {
el.style.display = 'none'
}, 20)
如果我们把dome1改成如上代码,然后再运行试试,就出现了“一闪而过”的现象。原因是我们在第一帧渲染完成后执行document.body.appendChild(el),第二帧的开头就会将el元素渲染出来,第二帧渲染完成后再执行el.style.display = 'none',第三帧的开头就会反应到页面上。
回头再看requestAnimationFrame运行在 下次重绘之前 这句话,可以理解为运行在在Javascript与Style之间的一个函数(暂不考虑浏览器差异,以W3C为标准)。如果把动画效果都打包进RAF的话, 就降低了因为不断操作同一dom产生动画不流畅(掉帧)的概率,渲染过程也会显得非常科学。当然,不可避免的在会有少数相关动画任务漏在了RAF回调之外执行,但问题也不会太大。RAF方案如下图:
需要注意的是RAF是一个macrotask(宏任务),callback中的代码中不宜包含复杂算法,否则还是会阻塞进程。想像一下如果第1帧的RAF回调算法过复杂导致原本1帧的任务用了2帧的时间才完成,那么第二帧的计算将会被拖到原本第三帧的位置,这样的话就相当于降低了页面刷新频率,可能会导致动画总时间变长。图解如下:
requestAnimationFrame基本用法
基本语法:
window.requestAnimationFrame(callback)。
参数:
callback,传入的函数可以通过js来控制下次重绘之前dom元素的css样式。callback默认接收一个时间参数,其数值等于performance.now(),一般用来控制动画效果。
返回值:
一个非零long整数,是回调列表中唯一的标识,没别的意义,跟setTimeout的返回值类似。我们可以传这个值给window.cancelAnimationFrame()取消这个回调函数。
const black = document.getElementById('black')
let animationId = null
let startTime = 0
let duration = 6000
let length = 600
let pass = 0
const getPosition = (x) => {
return Math.ceil(length * x)
}
const animations = (time) => {
let left = 0
if (time) {
if (time - startTime > duration + 100) {
startTime = time - pass
}
pass = time - startTime
left = getPosition(pass / duration)
}
if (left > length) {
black.style.left = `${length}px`
cancelAnimationFrame(animationId)
} else {
black.style.left = `${left}px`
animationId = requestAnimationFrame((timeStamp) => {
if (!startTime) startTime = timeStamp
animations(timeStamp)
})
}
}
black.addEventListener('click', () => {
animations()
})
上边代码中用setInterval或者setTimeout实现其实很简单,但这种匀速动画,属于最朴实无华的那种了,对于匀变速动画区别就体现的很明显了。很久以前瞄过几眼jquery代码,它的动画是用setTimeout完成的,听说后来也改成了RAF。setTimeout不无法把控页面刷新的频率,当完成匀变速动画或者速度变化复杂的动画的计算量和动画效果,也不如RAF。
requestAnimationFrame优劣
优势:
-
经过浏览器优化,动画更流畅。
-
间隔时间固定,只需要计算每一帧的变化。
-
窗口没激活时,动画将停止,省计算资源。
-
综合前三点优势,这种方案更省电,对移动终端来说会更友好。
劣势:
-
不同浏览器RAF的调用时机是不同的。有的浏览器的渲染过程如下图:
虽然调用时机不同但区别也并不是很大,只是每次动画开始执行的时候会有一帧的延迟,尝试了自己常用的三个浏览器尝试了一下。
firefox:RAF放在Style之前的执行;
Chrom:网上查到RAF放在Style之前执行;
Safari: RAF放在Compsite之后执行的;
- requestAnimationFrame兼容性问题及解决方法
在caniuser上搜索这个api,这个支持率还是非常可观的,但是为了满足个别怪物还需要加点代码来解决一下。
下面这段代码是搜来的,读起来并不是那么的困难,其实就是判断一下当前环境下window对象有没有requestAnimationFrame这个方法,没有的话就加'webkit'和'moz'前缀试试,如果还是没有的话那就用setTimeout来模拟一个。cancelAnimationFrame也是同理,实在没有的话就用clearTimeout来模拟。
if (!Date.now) Date.now = function() { return new Date().getTime(); };
(function() {
'use strict';
var vendors = ['webkit', 'moz'];
for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
var vp = vendors[i];
window.requestAnimationFrame = window[vp+'RequestAnimationFrame'];
window.cancelAnimationFrame = (window[vp+'CancelAnimationFrame']
|| window[vp+'CancelRequestAnimationFrame']);
}
if (/iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) // iOS6 is buggy
|| !window.requestAnimationFrame || !window.cancelAnimationFrame) {
var lastTime = 0;
window.requestAnimationFrame = function(callback) {
var now = Date.now();
var nextTime = Math.max(lastTime + 16, now);
return setTimeout(function() {callback(lastTime = nextTime); },
nextTime - now);
};
window.cancelAnimationFrame = clearTimeout;
}
}());