首发于码海拾贝

react fiber 搜罗整理篇

fiber 出台的原委和特性

The crux of the change is transitioning from processing updates in a synchronous, recursive way to relying on asynchronous scheduling and prioritization of interface changes.

A look inside React Fiber 指明,react 演进出 fiber 架构的关键点在于渲染策略的变更,从原先的 Stack Reconciler 同步递归模式转变为 Fiber Reconciler 基于优先级的异步调度模式。这样就能避免其他文章所说的,Stack Reconciler 模式中的 JS 运算、页面布局和页面绘制将长期占用主线程,页面出现掉帧的现象。依照官方文档 Codebase Overview,Fiber Reconciler 包含如下特性:

  • 将工作拆分为任务单元,实现增量渲染
  • 按类型为任务单元设置优先级
  • 任务单元可暂停、复用或中止
  • 任务单元可并发执行

完全理解 React Fiber 提到增量渲染所采用的 cooperative scheduling 合作式调度是操作系统 3 种任务调度策略中的一种。其内容为:将渲染工作拆解,每次只做一小段,做完看是否还有时间继续下一个任务,没有就把时间控制权交还给主线程,有就继续处理下一个任务。

React Fiber Architecture 也指出,这一处理机跟 call stack 将执行函数作为栈帧添加到堆栈中相似。在处理 ui 时,浏览器提供了可用的 api:requestIdleCallback 会调度在空闲期间调用的低优先级函数;requestAnimationFrame 会调度在下一个动画帧上调用的高优先级函数。在 React 中,单个 fiber 就如同栈帧。React 的调度策略基于 pull 模式智能应用更新(基于 push 模式需要由开发者自主决定哪些更新是要应用的),且为不同的 fiber 设置不同的优先级,可以避免不必要的 UI 更新。

React 分层模型和数据结构

推演 Inside Fiber: in-depth overview of the new reconciliation algorithm in React 提及的,React 各分层的数据处理走向比如:

  • 用户侧创建 Class 或 Function 组件、Host 组件(如 DOM 节点)、Protals 等;
  • 通过 React.createElement 创建 React 元素树,用于描述页面的呈现(Inside Fiber: in-depth overview of the new reconciliation algorithm in React 坚持认为 Virtual DOM 实际上指的是不可改变的 React 元素树);
  • 基于 React 元素树创建 fiber 节点树(也称为 internal instances 内部实例树);
  • 最后由 fiber 节点树获得具体平台相关的 public instance(也称为 Host instance,如真实的 DOM 树)。

上述数据结构的后半部分对应着 React 中的两个分层:

  • Reconciler 层:执行 reconciliation 流程,通过 diff 算法等将组件状态变化反应为视图内容,并触发 side-effects(调用生命周期方法、更新 ref)等。
  • Renderer 层:根据不同的平台,渲染出真实的内容,比较常见的是 ReactDOM、ReactNative。

fiber,作为 unit of work

fiber 基本结构

fiber 节点可基于 React 元素创建,通过调用 createFiberFromTypeAndProps.js 实现。实际上,创建 fiber 节点不止于基于 React 元素一种。fiber 节点会以单向链表的形式构成节点树。

export type Fiber = {|
  /** 1. 承接 React 元素的相关属性 **/
  tag: WorkTag,// fiber 节点的工作类型
  elementType: any,// react 元素类型
  key: null | string,// 用于 diff 算法的 key 键
  type: any,// 类组件构造函数或函数组件自身
  ref:// 引用
    | null
    | (((handle: mixed) => void) & {_stringRef: ?string, ...})
    | RefObject,

  /** 2. props & state **/
  // React 元素类型的引用,可以认为这个属性保存了与 fiber 相关的本地状态
  stateNode: any,
  pendingProps: any,// 待更新的 props
  memoizedProps: any,// 更新后的 props,作为输出透出到页面侧
  memoizedState: any,// 更新后的 state,作为输出透出到页面侧

  /** 存放 fiber 依赖的 contexts, events **/
  dependencies: Dependencies | null,

  /** 3. fiber 树结构 **/
  return: Fiber | null, // 类同堆栈的返回栈帧,通常是父节点
  child: Fiber | null,// 子节点
  sibling: Fiber | null,// 邻近的兄弟节点
  index: number,
|};

fiber 工作单元

fiber 节点树不像剥离掉 diff 算法后的 Virtual DOM 那样仅是一种表现,它内部集成了可调度工作任务以操作树的算法。Inside Fiber: in-depth overview of the new reconciliation algorithm in React 称单个 fiber 节点就是一个工作单元,这也是源码中的表现:

You can think of a fiber as a data structure that represents some work to do or, in other words, a unit of work. Fiber’s architecture also provides a convenient way to track, schedule, pause and abort the work.

current tree & work-in-progress tree

  • current tree:首次渲染或更新后设置,描述页面的当前展现;
  • work-in-progress tree:更新时用于页面呈现所需的操作,更新后会替代 current tree。

使用 work-in-progress tree 直接替代 current tree 称为 double buffering 双缓冲技术,这样便于复用 fiber 对象、节省内存分配和 GC 的时间开销。current tree、work-in-progress tree 处理的简要流程如下:

首次渲染时,渲染完成的 fiber 节点树称为 current tree;更新渲染时,fiber 节点将被重用,current tree 会维持不变,更新内容反应为另一个 fiber 节点树 work-in-progress tree。当所有渲染流程走完时,work-in-progress tree 会被刷新到页面上,并成为新的 current tree(即 current tree 表示已渲染内容;work-in-progress tree 表示处理中的渲染内容)。current tree、work-in-progress tree 中的 fiber 节点以 alternate 属性相互持有对方,这样结对的处理方式便于延后批量应用更新内容,支持工作任务的可打断。下文将表明,Fiber Reconciler 的更新机制分为两阶段,render 阶段处理并获得以 work-in-progress tree 呈现的更新,commit 阶段应用 work-in-progress tree 的更新。

update queue

可变的 state 更新会以 fiber.updateQueue 形式应用。ReactUpdateQueue.js 中阐明,UpdateQueue 跟 fiber 一样,也有 current queue、work-in-progress queue 两个队列。这两个队列共享同一个持久化的单链接链表。区别在于,这两个队列中指向活动中的 update 指针不同,work-in-progress 的指针索引在 render 阶段必然会大于或等于 current queue 的指针索引。提交阶段时,work-in-progress queue 会成为新的 current queue。任务的可中断正是在于,执行中的 work-in-progress queue 可被丢弃,并从 current queue 重新复制一份。

UpdateQueue 队列中的每个 update 任务有优先级标识。Reconciler 会根据优先级执行 update 任务。ReactUpdateQueue.js 指出需要特别注意的是,位于跳过的较低优先级之后高优先级任务仍旧会驻留在 UpdateQueue 队列中,就会造成这些高优先级任务被执行多次。正是因为这个原因,所以 render 阶段的生命周期函数才会打上 UNSAFE_ 标识,它们同样也会被执行多次,可能形成不必要的副作用(如在 UNSAFE_componentWillMount 中获取远程数据,请求就会发送多次)。

expiration time

fiber 任务的优先级与 fiber.expirationTime 息息相关。留待下回分解。

side effects

除了 state 更新以外,react 官方将更新 ref 引用、调用生命周期、订阅 state 变更 dom 等都视为 side effects。Inside Fiber: in-depth overview of the new reconciliation algorithm in React 引用了官方的原话:

You’ve likely performed data fetching, subscriptions, or manually changing the DOM from React components before. We call these operations “side effects” (or “effects” for short) because they can affect other components and can’t be done during rendering.
export type Fiber = {|
  // current tree、work-in-progress tree 中的 fiber 节点以 alternate 属性相互持有对方
  alternate: Fiber | null,

  // state 更新处理队列,以链表形式呈现
  updateQueue: UpdateQueue<any> | null,

  /** 优先级 **/
  expirationTime: ExpirationTime,// fiber 任务预期执行时间,不包含子树中的更新
  childExpirationTime: ExpirationTime,// 用于判断子树中的任务是否执行完成
  // 以下四字段在 enableProfilerTimer 为 true 时设置
  actualDuration?: number,// 当前 fiber 及子孙的渲染时间,rerender 时重置为 0
  actualStartTime?: number,// render 阶段,fiber 任务开始时间
  selfBaseDuration?: number,// 当前 fiber 的历史渲染时间
  treeBaseDuration?: number,// 当前 fiber 子孙节点的历史渲染时间

  // 用于描述 fiber 及其子树的特性,ConcurrentMode 表示默认情况下子树是否应异步
  // 创建 fiber 时,默认继承父节点的 mode,创建时可修改,fiber 生命周期中将维持不变
  mode: TypeOfMode,

  /** side-effects **/
  effectTag: SideEffectTag,
  nextEffect: Fiber | null,// 链表结构,指向下一个带有 side-effects 的 fiber
  firstEffect: Fiber | null,// 链表中的首节点,便于快速更新链表
  lastEffect: Fiber | null,// 链表中的尾节点,便于快速更新链表
|};

render、commit 两阶段渲染

如上文已指出,React 将渲染过程分为 render/reconciliation、commit 两阶段。完全理解 React Fiber 将这两阶段类比为 Virtual DOM 技术中的 diff、patch 过程。

  • render 阶段计算并获取以 work-in-progress tree 呈现的更新,可中断;
  • commit 阶段应用 work-in-progress tree 的更新。

React Fiber Architecture 中的以下描述也说明了两阶段的主要内容:

reconciliation
The algorithm React uses to diff one tree with another to determine which parts need to be changed.
update
A change in the data used to render a React app. Usually the result of setState. Eventually results in a re-render.

render 阶段

首次渲染时,reconciler 基于 React 元素创建 fiber,最终形成 fiber 节点树 current tree。当更新时,reconciler 会遍历 current tree,复用 fiber.alternate 备用节点,生成 work-in-progress tree。因为单个 fiber 也是 unit of work,work-in-progress tree 也可以称为任务单元树。

reconciler 会通过 performSyncWorkOnRoot、performConcurrentWorkOnRoot 函数启动 work-loop 循环 。该循环会使用深度优先算法处理 work-in-progress tree,只有经过更新的 fiber 节点才会被处理。work-loop 循环有以下四种处理函数:

  • performUnitOfWork:从 work-in-progress tree 中取出 fiber ,
  • beginWork:context 入栈后,更新当前 fiber 节点的 props、state 相关属性,视条件更新 side effects 链表,返回 fiber.child。
  • completeUnitOfWork:视条件更新 side effects 链表,返回 fiber.sibling 或 fiber.return。
  • completeWork:context 出栈等操作。

performUnitOfWork、completeUnitOfWork 主要用于迭代 fiber 节点,主要更新活动由 beginWork、completeWork 完成。以下是这四种函数迭代节点的流程:

function workLoopSync() {
  while (workInProgress !== null) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(workInProgress) {
  let next = beginWork(workInProgress);
  if (next === null) {
    next = completeUnitOfWork(workInProgress);
  }
  return next;
}

function beginWork(workInProgress) {
  console.log('work performed for ' + workInProgress.name);
  return workInProgress.child;
}

function completeUnitOfWork(unitOfWork) {
  workInProgress = unitOfWork;
  do {
    const current = workInProgress.alternate;
    const returnFiber = workInProgress.return;

    if ((workInProgress.effectTag & Incomplete) === NoEffect) {
      let next = completeWork(current, workInProgress, renderExpirationTime);

      if (next !== null) {
        return next;
      }
    } else if (returnFiber !== null) {
      const next = unwindWork(workInProgress, renderExpirationTime);

      if (next !== null) {
        return next;
      }
    }

    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      return siblingFiber;
    }
    // Otherwise, return to the parent
    workInProgress = returnFiber;
  } while (workInProgress !== null);
}

function completeWork(workInProgress) {
  console.log('work completed for ' + workInProgress.name);
  return null;
}

综上,更新时会生成部分节点带有 side effects 标识的 work-in-progress tree。effects list 链表会向上归并到父节点上,它会指示需要插入、更新或删除哪些节点,以及哪些组件需要调用其生命周期方法。current tree、work-in-progress tree、effects list 就是 render 阶段的所有产物(work-in-progress tree 在此时也被称为 finished-work tree)。

commit 阶段

commit 阶段的主要工作在于将 effects list 链表应用到 fiber 节点树上。这部分工作由 finishSyncRender、finishConcurrentRender 函数启动。主要功能则由 commitRootImpl.js 完成。Inside Fiber: in-depth overview of the new reconciliation algorithm in React 指出它包含如下操作:

  • 对标记了 Snapshot effect 的节点调用 getSnapshotBeforeUpdate 生命周期方法
  • 对标记了 Deletion effect 的节点调用 componentWillUnmount 生命周期方法
  • 执行所有 DOM 插入、更新和删除操作
  • 将 finished-work tree 分配给 FiberRoot,并置为 current tree
  • 对标记了 Placement effect 的节点调用 componentDidMount 生命周期方法
  • 对标记了 Update effect 的节点调用 componentDidUpdate 生命周期方法

后记

react fiber 是笔者长期没法啃下来的内容。这篇文章由以下文章汇总整理得来,却未对 scheduler、fiber 的优先级及类型等加诸说明。个中惭愧与骄傲处,留诗为念。

满身金紫一毛猴,偏腾赤手摘蟠桃。

不慎数声尽入水,引得御马都发笑。

Inside Fiber: in-depth overview of the new reconciliation algorithm in React

[译]深入React fiber架构及源码

React Fiber Architecture

完全理解 React Fiber

A look inside React Fiber

React Fiber 源码分析

发布于 02-22

文章被以下专栏收录