requestAnimationFrame与requestIdleCallback
requestAnimationFrame
0 浏览器渲染帧
如图代表一个时间轴,浏览器会每隔一小段时间把页面重新画一遍,把这个过程称之为渲染,间隔的时间点,称之为渲染帧,通常情况下,1 秒钟 60 帧(1 秒钟浏览器把页面画 60 遍)
如果用
js
计时器去做动画,可以精确的设置间隔时间,保证两个渲染帧之间,刚好夹一个动画操作(改变尺寸,位置等),好处在于,每一次改变之后就马上可以得到渲染,但是这一切都是理想的情况。
实际情况:渲染帧分布的没有那么平均,如下图所示。受影响的因素有很多,如机械配置等原因。
就会导致,有些地方,两个渲染帧之间没有做任何动画,即
空帧
(现在画了,但是跟之前画的一模一样,但是还是白画了一遍,没有在两个渲染帧之间做出任何动画操作,就造成了
渲染帧的浪费
)
同时,也会存在
跳帧
,两个渲染帧之间出现了多次动画(两个渲染帧之间,改变了两次位置,代码运行了,但是最后只渲染了一次,只是第二次的改变),页面上会感觉跳了一下,会导致
动画不连续
但是以上还是比较理想的情况,实际的情况更为复杂,比如你写的计时器 16 毫秒间隔,但是真的是 16 毫秒吗?
这样一来,
跳帧
、
空帧
就会非常严重。
此时,便引出了本文的重点:
requestAnimationFrame
1.1 官方定义
1.2 关于前端动画
1.2.1 前端动画方案目前有哪些?
css
动画
transition
:过渡动画
animation
:直接动画(搭配
@keyframes
)
js
动画
setInterval
或
setTimeout
定时器(比如不停地更改
dom元素
的位置,使其运动起来)
canvas
动画,搭配
js
中的定时器去运动起来(
canvas
只是一个画笔,然后我们通过定时器会使用这个画笔去画画-动画)
requestAnimationFrame动画(js动画中的较好方案)
1.2.2 为何要使用这个 api 来做动画?
在工作中,做动画最优的方案无疑是
css动画
,但是某些特定场景下,
css动画
无法实现我们所需要的需求。这时,我们就要考虑使用
js
去做动画了,
canvas动画
的
本质
也是
定时器动画
。使用定时器动画干活,实际上是可以的,但是存在一个最大的问题,就是如上文所讲到
动画会抖动
,体验效果不是非常好。
而使用
requestAnimationFrame
去做动画,就不会出现抖动的现象。
这里写了一个 demo 实现的效果图(gif 看着效果不太好,建议复制代码自己试一试)
1 |
|
1.3 语法规则
requestAnimationFrame
和
js
中的
setTimeout
定时器函数
基本一致
,不过
setTimeout
可以自由设置间隔时间,而
requestAnimationFrame
的间隔时间是由浏览器自身决定的,大约是
17毫秒
左右
requestAnimationFrame
我们可以在控制台输入
window
,然后展开查看其身上的属性,就能找到了
requestAnimationFrame
本质上是一个全局
window
对象上的一个属性函数,所以我们使用时,直接:
window.requestAnimationFrame(callBack)
即可。
callback
也是一个函数,即下一次重绘之前更新动画帧所调用的函数,即在这个函数体中,我们可以写对应的逻辑代码(和定时器类似)。
window.cancelAnimationFrame()来取消回调函数执行
,相当于定时器中的
clearTimeout()
。
setInterval
的效果,需要写成递归的形式(上述案例中也提到了)
1.4 关于卡顿的问题
1.4.1 为什么定时器会卡
卡了
,
PPT
的感觉
js
的事件队列宏任务、微任务影响,可能设定的是 17 毫秒执行一次,但是实际上这次是 17 毫秒、下次 21 毫秒、再下次 13 毫秒执行,所以并不是严格的卡住了这个 60HZ 的时间
变
、
不变
、
不变
、
变
、
不变
…
1.4.2 为何
requestAnimationFrame
不会卡
setTimeout
和
setInterval
的问题是,它们都不精确。它们的内在运行机制决定了时间间隔,参数实际上只是指定了把动画代码添加到浏览器
UI
线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。
而
requestAnimationFrame
是永远跟着渲染帧走的,无论渲染帧如何改变,始终保证在渲染帧之前始终有一个动画操作,既没有
空帧
也没有
跳帧
,从而保证了动画的流畅
1.5 应用场景
例如:回到顶部 组件,就可以使用
requestAnimationFrame
API 去做,理论讲解完毕,该自行实践了(doge)
requestIdleCallback
2.1 为什么需要 requestIdleCallback ?
在网页中,有许多耗时但是却又不能那么紧要的任务。它们和紧要的任务,比如对用户的输入作出及时响应的之类的任务,它们共享事件队列。如果两者发生冲突,用户体验会很糟糕。我们可以使用
setTimout
,对这些任务进行延迟处理。但是我们并不知道,
setTimeout
在执行回调时,是否是浏览器空闲的时候。
而
requestIdleCallback
就解决了这个痛点,
requestIdleCallback
会在帧结束时并且有空闲时间。或者用户不与网页交互时,执行回调。
2.2 API 简介
requestIdleCallback
的第一个参数时
callback
callback
被调用时,回接受一个参数
deadline
,
deadline
是一个对象,对象上有两个属性
timeRemaining
,
timeRemaining
属性是一个函数,函数的返回值表示当前空闲时间还剩下多少时间
didTimeout
,
didTimeout
属性是一个布尔值,如果
didTimeout
是 true,那么表示本次 callback 的执行是因为超时的原因
1 |
requestIdleCallback( |
2.3 空闲时间
requestIdleCallback
的 callback 会在浏览器的空闲时间运行,那么什么是空闲时间呢?
如上图。当我们在执行一段连续的动画的时候,第一帧已经渲染到屏幕上了,到第二帧开始渲染,这段时间内属于空闲时间。这种空闲时间会非常的短暂,如果我们的屏幕是 60hz(1s 内屏幕刷新 60 次)的。那么空闲时间会小于 16ms(1000ms / 16)。
另外一种空闲时间,当用户属于空闲状态(没有与网页进行任何交互),并且没有屏幕中也没有动画执行。此时空闲时间是无限长的。但是为了避免不可预测的事(用户突然和网页进行交互),空闲时间最大应该被限制在 50ms 以内。
为什么最大是 50ms?人类对 100ms 内的响应会认为是瞬时的。将空闲时间限制在 50ms 以内,是为了避免,空闲时间内执行任务,从而导致了对用户操作响应的阻塞,使用户感到明显的响应滞后。
在空闲期间,callback 的执行顺序是以 FIFO(先进先出)的顺序。但是如果在空闲时间内依次执行 callback 时,有一个 callback 的执行时间,已经将空闲时间用完了,剩下的 callback 将会在下一次的空闲时间执行。
1 |
const task1 = () => console.log("执行任务1");
|
如果当前的任务所需要的执行时间,超过了当前空闲时间周期内的剩余时间,我们也可以将任务带到下一个空闲时间周期内执行。在下一个空闲周期开始后,新添加的 callback 会被添加到 callback 列表的末尾。
1 |
const startTask = (deadline) { |
当我们网页处于不可见的状态时(比如切换到其他的 tag),我们空闲时间将会每 10s, 触发一次空闲期。
timeout
如果指定了 timeout,但是浏览器没有在 timeout 指定的时间内,执行 callback。在下次空闲时间时,callback 会强制执行。并且 callback 的参数,
deadline.didTimeout
等于 true,
deadline.timeRemaining()
返回 0。
1 |
requestIdleCallback((deadline) => { |
2.4 常见 Q&A
Q1:
requestIdleCallback
会在每一次帧结束时执行吗?