React Fiber reconciler

257 阅读5分钟

目标

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


浏览器的绘图流程


页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时( 1000ms/60 ≈ 16 ms,即每一帧工作量不超过16ms ),页面是流畅的,小于这个值时,用户会感觉到卡顿。



浏览器一帧的工作量如下:

image.png



浏览器每一帧的工作量有多有少,任务少的时候就会存在空闲期,如下:

12.9″ iPad Pro.png

假设有一段很长的任务(超过16ms)需要执行,很明显如果单独在浏览器的某一帧中执行该任务,因为超过了16ms,会导致用户出现卡顿。


该如何解决卡顿呢?


我们上面说到用户每一帧的工作量少的时候就会出现空闲期,如果我们把这段很长的任务拆分成一个个小任务,放到每一帧的空闲期去执行就可以解决这个问题,如下:

12.9″ iPad Pro3.png

如何将任务放入空闲期需要用到一个重要的api:window.requestIdelCallbackAPI

requestIdelCallbackAPI

语法

var handle = window.requestIdleCallback(callback[, options])


返回值

一个ID,可以把它传入 Window.cancelIdleCallback() 方法来结束回调。


参数

  • callback

一个在事件循环空闲时即将被调用的函数的引用。函数会接收到一个名为 IdleDeadline 的参数,这个参数可以获取当前空闲时间以及回调是否在超时时间前已经执行的状态。

  • options 可选

包括可选的配置参数。具有如下属性:

timeout:如果指定了timeout并具有一个正值,并且尚未通过超时毫秒数调用回调,那么回调会在下一次空闲时期被强制执行,尽管这样很可能会对性能造成负面影响。


实现:分成两步Reconciliation和commit


Reconciliation


基于上述原理,react Fiber要做的就是将更新任务拆分成一个个小任务,控制这些任务正常执行。


为了将渲染/更新可以拆分成一系列小任务,Fiber引入新的数据结构fiber node(在vdom tree上的扩展),不完全结构如下:


// Fiber对应一个组件需要被处理或者已经处理了,一个组件可以有一个或者多个Fiber
type Fiber = {|
  // 标记不同的组件类型
  tag: WorkTag,

  // ReactElement里面的key
  key: null | string,

  // ReactElement.type,也就是我们调用`createElement`的第一个参数
  elementType: any,

  // The resolved function/class/ associated with this fiber.
  // 异步组件resolved之后返回的内容,一般是`function`或者`class`
  type: any,

  // The local state associated with this fiber.
  // 保存组件的类实例、DOM节点或与Fiber节点关联的其他 React 元素类型的引用,
  // 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)
  stateNode: any,

  // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
  return: Fiber | null,

  // 单链表树结构
  // 指向自己的第一个子节点
  child: Fiber | null,
  // 指向自己的兄弟结构
  // 兄弟节点的return指向同一个父节点
  sibling: Fiber | null,
  index: number,

  // ref属性
  ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,

  // 新的变动带来的新的props
  pendingProps: any, 
  // 上一次渲染完成之后的props
  memoizedProps: any,

  // 该Fiber对应的组件产生的Update会存放在这个队列里面
  updateQueue: UpdateQueue<any> | null,

  // 上一次渲染的时候的state
  memoizedState: any,

  // 一个列表,存放这个Fiber依赖的context
  firstContextDependency: ContextDependency<mixed> | null,

  // 用来描述当前Fiber和他子树的`Bitfield`
  // 共存的模式表示这个子树是否默认是异步渲染的
  // Fiber被创建的时候他会继承父Fiber
  // 其他的标识也可以在创建的时候被设置
  // 但是在创建之后不应该再被修改,特别是他的子Fiber创建之前
  mode: TypeOfMode,

  // Effect
  // 用来记录Side Effect
  effectTag: SideEffectTag,

  // 单链表用来快速查找下一个side effect
  nextEffect: Fiber | null,

  // 子树中第一个side effect
  firstEffect: Fiber | null,
  // 子树中最后一个side effect
  lastEffect: Fiber | null,

  // 代表任务在未来的哪个时间点应该被完成
  // 不包括他的子树产生的任务
  expirationTime: ExpirationTime,

  // 快速确定子树中是否有不在等待的变化
  childExpirationTime: ExpirationTime,

  // 在Fiber树更新的过程中,每个Fiber都会有一个跟其对应的Fiber
  // 我们称他为`current <==> workInProgress`
  // 在渲染完成之后他们会交换位置
  alternate: Fiber | null,
|};


fiber tree 是通过 child、return、sibling 连接起来的:

image.png


react的更新过程即构造出新的fiber tree,新的fiber tree 称之为 workInProgress tree,workInProgress tree具体过程如下(以组件节点为例):


  1. 如果当前节点不需要更新,直接把子节点clone过来,跳到5;要更新的话打个tag
  2. 更新当前节点状态(props, state, context等)
  3. 调用shouldComponentUpdate(),false的话,跳到5
  4. 调用render()获得新的子节点,并为子节点创建fiber(创建过程会尽量复用现有fiber,子节点增删也发生在这里)
  5. 如果没有产生child fiber,该工作单元结束,把effect list归并到return,并把当前节点的sibling作为下一个工作单元;否则把child作为下一个工作单元
  6. 如果没有剩余可用时间了,等到下一次主线程空闲时才开始下一个工作单元;否则,立即开始做
  7. 如果没有下一个工作单元了(回到了workInProgress tree的根节点),第1阶段结束,进入pendingCommit状态


commit

将workInProgress tree 更新成current,页面重新渲染,得到新的页面。