React的Fiber调度和浏览器的帧

之前在如何渲染几万条数据不卡住界面时提到了一个BOM-API:requestAnimationFrame,这次因为React再提一次。

React的Fiber调度

在React v16中,React对调度策略进行了新的尝试,由原来的 Stack 改为使用 Fiber,对比原来的 Stack ,Fiber 主要具有以下的优点:

  • 能够把可中断的任务切片处理。
  • 能够调整优先级,重置并复用任务。
  • 能够在父元素与子元素之间交错处理,以支持 React 中的布局。
  • 能够在 render() 中返回多个元素。
  • 更好地支持错误边界。

Fiber-tree

Fiber 较为明显的是以 链表 的形式遍历的,在协调(reconcile)的过程中,分为2个阶段:

  • (可中断)render/reconciliation 通过构造workInProgress tree得出change
  • (不可中断)commit 应用这些DOM change

commit change实际上就是将比对出来的结果通过DOM展现出来,这一步是不可中断的,Fiber 的 workInProgress阶段是可以中断的,这也就是优先级的一种体现。

Fiber 调度的主要实现其实就是参考了BOM中的2个比较有意思的API实现的:requestIdleCallbackrequestAnimationFrame 。只是这2个是BOM中的原生API,React 则在自己的源码中自己hack了一套(本质其实是一样的)。

浏览器的帧

浏览器的工作机制 中,最后会有 的绘制过程,最后用户看到的网页,其实都是浏览器一帧一帧绘制而成的,当FPS(每秒绘制的帧数)达到60,对于人眼来说会呈现出流畅的效果,而如果FPS < 60,则会有卡顿的效果(玩过LOL、DOTA的小伙伴应该会很熟悉FPS、丢帧)。

所以为了达到流畅的效果,一帧应该绘制多少时间?

1000 / 60 ≈ 16ms,16毫秒

那在这一帧内,浏览器要做哪些工作呢?

frame

可以看出来,每一帧都会完成以下几个内容:

  • 用户交互
  • JS解析执行
  • 帧开始:包括窗口大小调整、滚动、动画等
  • rAF:包含 requestAnimationFrame
  • 布局
  • 绘制

可以看出,在完成每一帧的过程中,rAF固定进行的一步,所以 requestAnimationFrame 每一帧必定会执行。

requestAnimationFrame

window.requestAnimationFrame(callback);
  • Callback: 下一次重绘之前更新动画帧所调用的函数
  • 返回一个long整数,可用于window.cancelAnimationFrame() 取消回调函数

它由系统来决定回调函数的执行时机,浏览器会在下一次重新渲染之前请求执行回调函数。无论设备的刷新率是多少,requestAnimationFrame 的时间间隔都会紧跟屏幕刷新一次所需要的时间;例如某一设备的刷新率是 75 Hz,那这时的时间间隔就是 13.3 ms(1 秒 / 75 次)。

需要注意的是这个方法虽然能够保证回调函数在每一帧内只渲染一次
但是如果这一帧有太多任务执行,还是会造成卡顿的; 因此它只能保证重新渲染的最短时间间隔是屏幕的刷新时间。

具体的使用demo可以查看:如何渲染几万条数据不卡住界面

requestIdleCallback

刚才说16ms 是完成一帧的理想值,那么如果在一帧结束时没有耗满16ms,比如4ms、10ms,那么浏览器会选择在剩余的空闲时间内执行requestIdleCallback 内的任务。

当然如果没有空闲时间,则不会执行requestIdleCallback

具体使用如下:

var handle = window.requestIdleCallback(callback[, options])
  • Callback: 空闲时间调用的函数,接收一个IdleDeadLine参数,获取当前空闲时间以及回调是否在超时时间前已经执行的状态。
  • Option: 配置timeout参数,如果超过这个时间任务还未被执行,则在下一帧空闲时间强制执行
requestIdleCallback(myNonEssentialWork, { timeout: 2000 });

// 任务队列
const tasks = [
 () => {
   console.log("第一个任务");
 },
 () => {
   console.log("第二个任务");
 },
 () => {
   console.log("第三个任务");
 },
];

function myNonEssentialWork (deadline) {
 // 如果帧内有富余的时间,或者超时
 while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
   work();
 }

 if (tasks.length > 0)
   requestIdleCallback(myNonEssentialWork);
 }

function work () {
 tasks.shift()();
 console.log('执行任务');
}

所以在优先级方面: requestAnimationFrame > requestIdleCallback,一些低优先级的任务是可以使用 requestIdleCallback 去执行,但是考虑到它执行的时机是有限的,所以最好此时的任务是能够量化、细分的微任务。

注意:

  • 尽可能少的在requestIdleCallback里执行有关DOM操作的任务,因为这个帧的布局已经绘制完成,如果操作了DOM可能会导致重绘,那么前面的帧可能就是在浪费开销;在绘制之前的requestAnimationFrame中执行即可,因为它是在这一帧绘制前执行的。
  • Promise尽可能少的放在此时执行,因为它的优先级太高,会被浏览器判定需要立即执行,即使当前这一帧的requestIdleCallback已经把空闲时间耗尽,也会继续执行Promise的任务,从而导致该帧大于16ms

总结

所以当 React 采用 Fiber 架构后,它就可以有 增量渲染、优先级、更新时暂停、终止和复用 的能力,具体表现出来就是变得 更流畅,交互更友好了。

Fiber


1 + 4 =

求知若飢,虛心若愚。