requestAnimationFrame 的基本使用

requestAnimationFrame 是什么

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行 顾名思义,请求动画帧,也称 帧循环。 其实就是该API能以浏览器的显示频率来作为其动画动作的频率,比如浏览器每16.7ms刷新一次,动画回调也每16.7ms调用一次,这样就不会存在过度绘制的问题,动画不会掉帧,自然流畅。

requestAnimationFrame 怎么使用

先来个例子

//html代码
<body><h1>requestAnimationFrame API</h1><button id='begin' class="begin">开始</button><button id='end' class="end">停止</button>
</body>

//js
(() => {function test() {console.log('hello ~ requestAnimationFrame');}requestAnimationFrame(test)
})() 

可以看到,控制台成功的输出了一次 log 。 但是它只执行了一次,怎么做动画呢?别急,再看看 MDN 怎么说。

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

修改代码再试一下。

(() => {let n = 0function test() {n++console.log(`hello ~ requestAnimationFrame ${n}`);requestAnimationFrame(test)}requestAnimationFrame(test)
})() 

打开控制器可以看到一种在执行

执行频率

回调函数执行次数通常是每秒 60 次,但在大多数遵循 W3C 建议的浏览器中,回调函数执行次数通常与 浏览器屏幕刷新次数 相匹配。 屏幕刷新频率(次数): 屏幕每秒出现图像的次数。普通笔记本为60Hz。

回调参数

回调函数会被传入DOMHighResTimeStamp参数,DOMHighResTimeStamp指示当前被 requestAnimationFrame() 排序的回调函数被触发的时间。

(() => {function test(timestamp) {console.log(`🚀🚀hello ~ requestAnimationFrame ${timestamp}`);requestAnimationFrame(test)}requestAnimationFrame(test)
})() 

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

(() => {function test1(timestamp) {console.log(`hello ~ requestAnimationFrame1 ${timestamp}`);requestAnimationFrame(test1)}function test2(timestamp) {console.log(`ello ~ requestAnimationFrame2 ${timestamp}`);requestAnimationFrame(test2)}requestAnimationFrame(test1)requestAnimationFrame(test2)

})() 

可以看到,两个 requestAnimationFrame 在控制台输出的时间戳是一样的。也就是浏览器刷新一次的时候,执行所有的 requestAnimationFrame ,并且它们的回调参数是一模一样的。

ref

requestAnimationFrame()的返回值(上文我称为ref),也是有作用的,代表requestAnimationFrame回调执行的次数, 每执行一次,数值就会 +1

let ref = requestAnimationFrame(one)
function one() { console.log(ref)ref = requestAnimationFrame(one)
} 

终止执行

window.cancelAnimationFrame() 以取消回调函数。 那如果我想要在特定的条件下终止 requestAnimationFrame 怎么办呢,官方也给出了答案,那就是 cancelAnimationFrame API 。 只需要把 requestAnimationFrame 的返回值作为参数传递给 cancelAnimationFrame 就可以了

(() => {const beginBtn = document.querySelector("#begin")const endBtn = document.querySelector("#end")let myRef;beginBtn.addEventListener("click", () => {myRef = requestAnimationFrame(test)})endBtn.addEventListener("click", () => {cancelAnimationFrame(myRef)})function test() {myRef = requestAnimationFrame(test)console.log('~ myRef:', myRef);}
})() 

其实不用这个 API 也可以达到终止执行的目的,比如简单的 if语句 。

(() => {function test(timestamp) {console.log(`hello ~ requestAnimationFrame ${timestamp}`);if (timestamp < 500) {requestAnimationFrame(test)}}requestAnimationFrame(test)
})() 

使用requestAnimationFrame 和 cancelAnimationFrame

所以可以利用 requestAnimationFrame 和 cancelAnimationFrame做一些事情 比如 累积一定时间可以做一些操作

(() => {let startTime = Date.now();function handleTicker() {foo(Date.now() - startTime);startTime = Date.now();requestAnimationFrame(handleTicker);}requestAnimationFrame(handleTicker);let t = 0function foo(timeInterval) {t += timeIntervalconsole.log('~ t:', t);if (t > 1000) {console.log('~ 做事情');t = 0}}
})() 

简单动画演示

<style> #box {width: 0px;height: 50px;background-color: blue;} </style>
<body><h1>requestAnimationFrame API</h1><button id='begin' class="begin">开始</button><button id='end' class="end">停止</button><div id='box'></div>
</body> 
(() => {const beginBtn = document.querySelector("#begin")const endBtn = document.querySelector("#end")const box = document.querySelector("#box")let myRef;beginBtn.addEventListener("click", () => {myRef = requestAnimationFrame(test)})endBtn.addEventListener("click", () => {cancelAnimationFrame(myRef)})function test() {box.style.width = `${myRef}%`myRef = requestAnimationFrame(test)}
})() 

深入了解 requestAnimationFrame

requestAnimationFrame 与 setInterval setTimeout

setTimeout 和 setInterval 的问题是,它们不够精确。它们的内在运行机制决定了 时间间隔参数 实际上只是指定了把动画代码添加到 浏览器UI线程队列 中以等待执行的时间。如果队列前面已经加入了其它任务,那动画代码就要等前面的 任务完成后 再执行,并且如果时间间隔过短(小于16.7ms)会造成丢帧,所以就会导致动画可能不会按照预设的去执行,降低用户体验。

requestAnimationFrame 采用 浏览器时间间隔 ,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,消耗性能;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个 统一 的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。

  • 这个其实和 浏览器的事件循环机制是有关系的,在与他们之间执行的方式不同,下边会有介绍 requestAnimationFrame和EventLoop之间的关系

CSS3 的transition 和 animation 搭配使用可以说是非常强大了,但是也有的触手伸不到的地方,比如说 scrollTop,另外 CSS3 动画支持的贝塞尔曲线也是有限的。 那么,CSS3 做不到的就可以用到 requestAnimationFrame 来解决了。

性能对比

来个例子

<div id="test" style="width: 0px; height: 12px; line-height: 12px; margin-bottom: 5px; background: rgb(185, 236, 243);"></div>
当前进度:<span id="progress">0%</span>
<button id="btn">开启</button>
<script> const btn = document.getElementById('btn');
//使用 requestAnimationFrame 实现
btn.onclick = function() {var timer = requestAnimationFrame(function fn() {if (parseInt(test.style.width) < 300) {test.style.width = parseInt(test.style.width) + 3 + 'px';progress.innerHTML = parseInt(test.style.width) / 3 + '%';timer = requestAnimationFrame(fn);} else {cancelAnimationFrame(timer);}});
}
//使用 setInterval 实现
// btn.onclick = function() {
// var timer = setInterval(function () {
// if (parseInt(test.style.width) < 300) {
// test.style.width = parseInt(test.style.width) + 3 + 'px';
// progress.innerHTML = parseInt(test.style.width) / 3 + '%';
// } else {
// clearInterval(timer);
// }
// }, 17);
// }

//使用 setTimeout 实现
//btn.onclick = function() {
// var timer = setTimeout(function fn() {
// if (parseInt(test.style.width) < 300) {
// test.style.width = parseInt(test.style.width) + 3 + 'px';
// progress.innerHTML = parseInt(test.style.width) / 3 + '%';
// timer = setTimeout(fn, 17);
// } else {
// clearTimeout(timer);
// }
// }, 17);
// } </script> 

使用setTimeout和setInterval的帧率

使用requestAnimationFrame的帧率

  • 注: 我认为这里可以看 FPS 和GPU的渲染 比较他们渲染方式不同

requestAnimationFrame 和事件循环

Event Loop(事件循环)是用来协调事件、用户交互、脚本、渲染、网络的一种浏览器内部机制。

Event Loop 在浏览器内也分几种:

  • window event loop
  • worker event loop
  • worklet event loop

我们这里主要讨论的是 window event loop。也就是浏览器一个渲染进程内主线程所控制的 Event Loop。 然后大致讲下流程 在这里不详细说明 eventLoop

Event Loop的基本处理过程

1.在所选 task queue (taskQueue)中约定必须包含一个可运行任务。如果没有此类 task queue,则跳转至下面 microtasks 步骤。
2.让 taskQueue 中最老的 task (oldestTask) 变成第一个可执行任务,然后从 taskQueue 中删掉它。
3.将上面 oldestTask 设置为 event loop 中正在运行的 task。
4.执行 oldestTask。
5.将 event loop 中正在运行的 task 设置为 null。
6.执行 microtasks 检查点(也就是执行 microtasks 队列中的任务)。
7.设置 hasARenderingOpportunity 为 false。
8.更新渲染。
9.如果当前是 window event loop 且 task queues 里没有 task 且 microtask queue 是空的,同时渲染时机变量 hasARenderingOpportunity 为 false ,去执行 idle period(requestIdleCallback)。
10.返回到第一步。 下图 是 event loop 在浏览器主线程上运行的一个清晰的流程:

在上面规范的说明中,渲染的流程是在执行 microtasks 队列之后,更进一步,再来看看渲染的处理过程。

更新渲染过程

遍历当前浏览上下文中所有的 document ,必须按在列表中找到的顺序处理每个 document 。 渲染时机(Rendering opportunities):如果当前浏览上下文中没有到渲染时机则将所有 docs 删除,取消渲染(此处是 否存在渲染时机由浏览器自行判断,根据硬件刷新率限制、页面性能或页面是否在后台等因素)。 如果当前文档不为空,设置 hasARenderingOpportunity 为 true 。 不必要的渲染(Unnecessary rendering):如果浏览器认为更新文档的浏览上下文的呈现不会产生可见效果且文档的 animation frame callbacks 是空的,则取消渲染。(终于看见 requestAnimationFrame 的身影了 从 docs 中删除浏览器认为出于其他原因最好跳过更新渲染的文档。 如果文档的浏览上下文是顶级浏览上下文,则刷新该文档的自动对焦候选对象。 处理 resize 事件,传入一个 performance.now() 时间戳。 处理 scroll 事件,传入一个 performance.now() 时间戳。 处理媒体查询,传入一个 performance.now() 时间戳。 运行 CSS 动画,传入一个 performance.now() 时间戳。 处理全屏事件,传入一个 performance.now() 时间戳。 执行 requestAnimationFrame 回调,传入一个 performance.now() 时间戳。 执行 intersectionObserver 回调,传入一个 performance.now() 时间戳。 对每个 document 进行绘制。 更新 ui 并呈现。

流程基本如下图所示

至此,requestAnimationFrame 的回调时机就清楚了,它会在 style/layout/paint 之前调用。

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

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

下图是 setTimeout 执行情况,红色圆圈处是两次渲染,中间四次是处理 setTimout task,因为屏幕的刷新频率是 60 Hz,所以大致在 16.6ms 之内执行了多次 setTimeout task 之后才到了渲染时机并执行渲染。

requestAnimationFrame 帧动画不同之处在于,每次渲染之前都会调用,此时设置的 marginLeft 和上一次渲染前 marginLeft 的差值为 1px 。

下图是 requestAnimationFrame 执行情况,每次调用完都会执行渲染:

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 的出现让我们有了除了这两种我们常用的方案之外的另一种更优的选择

其他知识 (仅了解)

即图像在屏幕上更新的速度,也即屏幕上的图像每秒钟出现的次数,它的单位是赫兹(Hz)。 对于一般笔记本电脑,这个频率大概是60Hz, 可以在桌面上 右键 > 屏幕分辨率 > 高级设置 > 监视器 中查看和设置。这个值的设定受屏幕分辨率、屏幕尺寸和显卡的影响,原则上设置成让眼睛看着舒适的值都行。 市面上常见的显示器有两种,即 CRT和 LCD, CRT 是一种使用阴极射线管(Cathode Ray Tube)的显示器,LCD 就是我们常说的液晶显示器( Liquid Crystal Display)。 CRT 是一种使用阴极射线管的显示器,屏幕上的图形图像是由一个个因电子束击打而发光的荧光点组成,由于显像管内荧光粉受到电子束击打后发光的时间很短,所以电子束必须不断击打荧光粉使其持续发光。电子束每秒击打荧光粉的次数就是屏幕绘制频率。 而对于 LCD 来说,则不存在绘制频率的问题,因为 LCD 中每个像素都在持续不断地发光,直到不发光的电压改变并被送到控制器中,所以 LCD 不会有电子束击打荧光粉而引起的闪烁现象。 因此,当你对着电脑屏幕什么也不做的情况下,显示器也会以每秒60次的频率正在不断的更新屏幕上的图像。为什么你感觉不到这个变化? 那是因为人的眼睛有视觉停留效应,即前一副画面留在大脑的印象还没消失,紧接着后一副画面就跟上来了,这中间只间隔了16.7ms(1000/60≈16.7), 所以会让你误以为屏幕上的图像是静止不动的。而屏幕给你的这种感觉是对的,试想一下,如果刷新频率变成1次/秒,屏幕上的图像就会出现严重的闪烁,这样就很容易引起眼睛疲劳、酸痛和头晕目眩等症状。

  • CSS 动画原理

根据上面的原理我们知道,你眼前所看到图像正在以每秒 60 次的频率绘制,由于频率很高,所以你感觉不到它在绘制。而 动画本质就是要让人眼看到图像被绘制而引起变化的视觉效果,这个变化要以连贯的、平滑的方式进行过渡。 那怎么样才能做到这种效果呢? 60Hz 的屏幕每 16.7ms 绘制一次,如果在屏幕每次绘制前,将元素的位置向左移动一个像素,即1px,这样一来,屏幕每次绘制出来的图像位置都比前一个要差1px,你就会看到图像在移动;而由于人眼的视觉停留效应,当前位置的图像停留在大脑的印象还没消失,紧接着图像又被移到了下一个位置,这样你所看到的效果就是,图像在流畅的移动。这就是视觉效果上形成的动画。

最后

整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

更多推荐