面试官:你了解requestAnimationFrame吗?

826 阅读7分钟

面试官:你知道 setTimeout 是怎么运行的吗?

我:巴拉巴拉······(扯了一大堆宏任务和微任务的相关机制)

面试官:那你知道requestAnimationFramesetTimeout 有什么区别吗?

我:额···我不是很了解这个东西,好像react的 requestIdleCallback 和这个差不多???

面试官:你刚刚有说 setTimeout 因为机制的原因不能稳定的执行,而 requestAnimationFrame 可以相对稳定,为什么呢?

我:···

前言

这是今年秋招小红书的面试题,requestAnimationFrame 这个api以及动画的不了解,导致在这个问题上被血虐,知耻而后勇,于是面完赶紧恶补了一番,了解到这个东西,在这里总结一下所学的内容:

动画

首先在早期的动画就是用setTimeout和setInterval来实现的,css3 带来了一个新的动画过度属性 transition.对于 transition 能实现动画很好的过度效果。

但是对于 setTimeout , setInterval 来言. 虽然有了定时的效果,但是定时的时间不是那么准确,这是因为js的Eventloop机制,因为JavaScript的环境在浏览器是单线程,所以setTimeout要等所有同步任务执行完才会执行,但是也不确定什么时候同步任务可以执行完,所以延迟多久也是不能保证的。这也是为什么setTimeout在有些情况不能保证运行的时间。

在MDN中是这样解释requestAnimationFrame的:

window.requestAnimationFrame()  告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

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

requestAnimationFrame的回调函数并不能被重复调用,这点和 setInterval 不同,它和 setTimeout 类似,回调函数只能被调用一次,只不过 setTimeout 可以自定义调用时间, requestAnimationFrame的调用时间则是跟着系统的刷新频率走的,所以在实现动画的时候,setTimeoutrequestAnimationFrame 更加灵活, requestAnimationFramesetTimeout 表现效果更加优秀。

以在3000毫秒内移动1500px距离的动画为例

setTimeout的实现方式

<div id="div" style="width:100px; height:100px; background-color:#000; position: absolute;left:0; top:0;">
</div>

<script type="text/javascript">
let divEle = document.getElementById("div");
const distance = 1500;
const timeCount = 3000;
const intervalTime = 10;
let runCount = timeCount / intervalTime;
let moveValue = distance / runCount;

function handler() {
    let left = parseInt(divEle.style.left);
    if(left >= distance) {
        return;
    }
    divEle.style.left = left + moveValue;
    window.setTimeout(handler, intervalTime);
}

window.setTimeout(handler, intervalTime);
</script>

以上代码通过setTimeout每10毫秒为间隔时间改变一次元素的位置以实现元素的动画效果, 当然, 可以通过改变这个间隔时间来微调动画效果,可是你永远没有办法确定最优方案,因为它总会和刷新频率存在交叉。

通过requestAnimationFrame我们可以给出更好的解决方案

<div id="div" style="width:100px; height:100px; background-color:#000; position: absolute;left:0; top:0;">
</div>

<script type="text/javascript">
let divEle = document.getElementById("div");
const distance = 1500;
const timeCount = 3000;
function handler( time ) {
    if(time > timeCount) {
        time = timeCount;
    }
    divEle.style.left = time * distance / timeCount;  
    window.requestAnimationFrame( handler );
}

 window.requestAnimationFrame( handler );
</script>

如果setTimeout,handler函数也会被递归的重复调用,只是它的调用和显示的刷新频率是一致的,因此动画效果更加顺滑自然,也能找到性能和效果的最佳均衡点,得到最有的解决方案。

总结:

  1. 浏览器的刷新机制是客观存在的.
  2. requestAnimationFrame 仅仅只是在浏览器每一次刷新渲染新帧之前的一个切片或者说一个时机.你用或者不用,这个切片都在那里.
  3. 它和你是否执行动画效果,还是单纯的业务逻辑代码并没强求.我们可以将任意代码都丢在这个切片里面执行.
  4. 有时候,一些业务代码要求的是执行效率,丢在在这个切片里,受制于浏览器刷新性能,反而会拖慢整个流程的运行.

requestIdleCallback和requestAnimationFrame有什么区别?

讲到这就可以重新理一遍requestIdleCallbackrequestAnimationFrame有什么区别:

  • 我们所看到的网页,都是浏览器一帧一帧绘制出来的,通常认为FPS为60的时候是比较流畅的,而FPS为个位数的时候就属于用户可以感知到的卡顿了,那么在一帧里面浏览器都要做哪些事情呢,如下所示: image.png 图中一帧包含了用户的交互、js的执行、以及requestAnimationFrame的调用,布局计算以及页面的重绘等工作。

假如某一帧里面要执行的任务不多,在不到16ms(1000/60)的时间内就完成了上述任务的话,那么这一帧就会有一定的空闲时间,这段时间就恰好可以用来执行requestIdleCallback的回调,如下图所示:

在这里插入图片描述

当程序栈为空页面无需更新的时候,浏览器其实处于空闲状态,这时候留给requestIdleCallback执行的时间就可以适当拉长,最长可达到50ms,以防出现不可预测的任务(用户输入)来临时无法及时响应可能会引起用户感知到的延迟。

由于requestIdleCallback利用的是帧的空闲时间,所以就有可能出现浏览器一直处于繁忙状态,导致回调一直无法执行,这其实也并不是我们期望的结果(如上报丢失),那么这种情况我们就需要在调用requestIdleCallback的时候传入第二个配置参数timeout了

time-slicing时间切片

浏览器一帧执行的任务

image.png

requestIdleCallback(myNonEssentialWork, { timeout: 2000 });

function myNonEssentialWork (deadline) {
  // 当回调函数是由于超时才得以执行的话,deadline.didTimeout为true
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
         tasks.length > 0) {
       doWorkIfNeeded();
    }
  if (tasks.length > 0) {
    requestIdleCallback(myNonEssentialWork);
  }
}
  • 如果是因为timeout回调才得以执行的话,其实用户就有可能会感觉到卡顿了,因为一帧的执行时间必然已经超过16ms了

再次总结

  • requestAnimationFrame的回调会在每一帧确定执行,属于高优先级任务,
  • requestIdleCallback的回调则不一定,属于低优先级任务。

React fiber

既然聊到了React的fiber,那就再了解一下fiber机制又是怎么根据requestIDCallback来实现的:

  • 同步更新的局限性

在React v16版本之前,React的更新过程是通过递归从根组件树开始同步进行的,当组件数很大时,就会出现卡顿的问题。

于是在React 16.8版本加入了fiber

React 框架内部的运作可以分为 3 层:

  • Virtual DOM 层,描述页面长什么样。
  • Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。
  • Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative

reconciliation期间,来自render方法返回的每个React元素的数据被合并到fiber node树中,每个React元素(虚拟dom)都有一个相应的fiber node,这些可变的数据包含组件state和DOM。 根据React元素的类型,框架需要执行不同的活动。在我们的示例应用程序中,对于class组件ClickCounter,它调用生命周期方法和render方法,而对于span Host 组件(DOM节点),它执行DOM更新。因此,每个React元素都会转换为相应类型的fiber节点,用于描述需要完成的工作

当React元素第一次转换为fiber节点时,React使用createElement返回的数据来创建fiber

这次改动最大的当属 Reconciler 层了,React 团队也给它起了个新的名字,叫Fiber Reconciler。这就引入另一个关键词:Fiber。

requestIdleCallback((deadline)=>{
   	deadline.timeRemaining()	一帧中剩余的时间
   	deadline.didTimeout		是否已超时timeout
   	timeout		                 设置当n毫秒后,强制执行任务
},timeout);

但是react并未使用原生requestIdleCallback,而是使用扩展实现的原因

  • 回调并不会严格执行,比如在tab页切换后触发频率会降低
  • 兼容性不行

react在不支持requestIdleCallback的浏览器中,通过requestAnimation+MessageChannel模拟实现。

总之而Fiber架构就是为了支持“可中断渲染”而创建的。在React中,fiber tree是一种数据结构,它可以把虚拟dom tree转换成一个链表,从而可以在执行遍历操作时支持断点重启。

参考文章

requestAnimationFrame的作用及使用

# 前端-requestIdleCallback和requestAnimationFrame