帧对齐方案react scheduler

1,072 阅读3分钟

scheduler.png

这一部分主要讨论的是如何实现帧对齐,而不会过多的去探究旧版方案的调度流程。如果想了解可以直接看源码,或者去到这里scheduler

必要的回顾

早在之前的文章中介绍了requestAnimation的几个重要的特性:

window.requestAnimationFrame()

  1. 该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
  2. 在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数已经消耗了一些时间。
  3. 在大多数遵循W3C建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配

frame.png

上图为chrome performance面板中获取到的各个帧的截图,红框的长度表示一帧间隔,同一个红框当中执行的requestAnimation回调函数接收到的时间戳都是相同的值。一般而言,对于一个刷新频率为60hz的屏幕来说,回调函数会在16.7ms之后执行。那么react-scheduler则需要动态的识别浏览器的刷新频率。

新老scheduler的对比

scheduler.png

先简单介绍frame对齐方案的基本流程,如上图所示,我们在通过scheduleCallback添加任务的时候会调用requestCallback,这个函数会通过判断最高优先级的任务是否已经到期了,如果到期了会在下一个事件循环中通过idleTick调用flushWork去刷新链表中(新版的优先队列使用最小堆来实现时间复杂度从n提升到logn)中已到期的任务。并且如果还有任务存留则会调用requestAnimation,目的是在下一次重绘之前把剩余的任务检查一下,看有没有任务需要通过idleTick执行。

此外需要注意一点,在flushWork不断执行过期任务的过程中,是不是可以将所有到期的任务都执行完成呢?并非如此,在任务执行时间比较短的前提下(代码质量高),如果任务非常多,那么也可能会阻塞当前帧的渲染,导致掉帧而卡顿。因此在flushWork的过程中,每次任务执行之前都需要查看是否已经到了屏幕需要刷新下一帧的时间点。如果到了那个时间点,那么不能再执行下一个任务了,需要通过创建一个宏任务,将主线程让出来给渲染进程来渲染页面。(这里也可以看到新老版本的scheduler的区别,新版本以5ms为一个时间片段,将空闲时间划分出了更多的时间片段,这样使得有更多的时间间隔来响应用户的操作,变得更加reactive)。

同时为了保证任务的有序进行,在每一帧中react-scheduler通过requestAnimation注册的回调函数只能存在一个。

动态获取屏幕的帧率

// var nextFrameTime = activeFrameTime‘ + activeFrameTime;
var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
if (
  nextFrameTime < activeFrameTime && // activeFrameTime‘ < 0
  previousFrameTime < activeFrameTime // 
) {
  if (nextFrameTime < 8) {
    nextFrameTime = 8;
  }
  activeFrameTime =
    nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
} else {
  previousFrameTime = nextFrameTime;
}
frameDeadline = rafTime + activeFrameTime;

nextFrameTime这里其实就是当前的帧率表示的帧之间的时间间隔。在requestAnimation的回调函数中,会计算出屏幕的下一帧时间点frameDealine,其中rafTime是当前回调函数接收到的时间,frameDeadline为上一个requestAnimation回调函数预测的时间点。

在这个条件语句中,这一帧当前真实的时间点rafTime比预测的时间点小,也就是说帧率预测过低,时间与频率成反比。那么我们需要接受这个帧率,即重新设置帧的时间间隔为previousFrameTimenextFrameTime的最大值。

如果帧率预测过高,即时间间隔预测过低,那么将此次的时间间隔给保存到previousFrameTime

总结

activeFrameTime初始值为33,并且当实际的时间间隔在连续两次都小于当前activeFrameTime的时候才会更新activeFrameTime。所以activeFrameTime的取值为833ms,对应的频率范围是30hz120hz,并且activeFrameTime在减少之后无法再增加。因此旧版的scheduler只能够获取到当前系统屏幕的帧率,并且只能够向上调整帧率(即向下调整时间间隔)。

老版本scheduler的缺点:

  1. 时间片切分不够细:一旦处于刷新任务的循环中,在任务足够多的情况下,直到没有空闲时间才会停止刷新任务,这样导致了一整段空闲时间都阻塞了渲染进程。
  2. 依赖帧率:一旦出现帧率设置的小于实际的帧率,而无法向上调整,因此会出现过期时间比实际的小,从而空闲时间时间变小,无法充分利用所有的空闲时间来执行任务。

为了解决上述问题,新版的scheduler采用5ms的时间切片,每5ms释放一下主线程控制权,对用户的操作更加灵敏,解决了第一个问题。并且不再采用帧对齐方案,在有任务的时候,利用宏任务紧凑的刷新任务队列,充分的利用了空闲时间,解决了第二个问题。

新版本scheduler的方案可以看公众号的前一篇文章。