react fiber 遍历模型

Liu Bowen

Liu Bowen / 2021, 三月, 15

react fiber 是基于要解决可中断的异步协调算法 fiber reconciliation 的而衍生出的一种基本数据结构,fiber node 保存了与对应 react element node 不限于副作用相关的 元数据 信息,进而为异步协调算法提供协调依据。

A Fiber is work on a Component that needs to be done or was done. There can be more than one per component.#

为什么需要每个 react element node 都对应至少一个 fiber node?

fiber node 定位于为 react element node 提供元数据信息,并且是协调算法的主要协调对象,基本的操作单元。那么为了能够完整的协调对比出当前需要更新的节点,故每个 react element node 都要关联一个 fiber node 来提供协调元数据信息;

react fiber 要解决的问题

react 将 react fiber 作为更新的单元节点,将原有的 stack reconciler 中的基于 stack 结构的节点遍历改为 fiber reconciler 中的多链表遍历。通过多链表的组合遍历规避了原有深度优先遍历直接对 stack 数据结构的使用。链表遍历天生具有可中断,仅需要保存中断时的节点即可恢复遍历的特点。细节可见另一篇博文——基于单向链表的 fiber reconciler

遍历理论基础

在数据结构关系中,树的特例是链表,那么对于整个 fiber tree 我们可以认为是多条多种链表构成的组合体。结合 fiber 的设计定位,为了完整的表示 react element tree,fiber 之间应该存在父子,兄弟,子父关系。

  1. 定义由 fiber.child 构成的 父 -> 子 链表;

  2. 定义由 fiber.sibling 构成的 兄 -> 弟 链表;

  3. 定义由 fiber.return 构成的 子 -> 父 链表。

在整个 react fiber tree 中,并非仅有三条链表,而是存在三类链表。

对于常规的链表遍历,有如下基本思维模型:

ts
interface ListNode {
  next: ListNode
}

function linkedListTraversal(head: ListNode) {
  while (workInProgress !== null) {
    workInProgress = workInProgress.next
  }
}

其核心在于借助循环体结构,不断地向 next 链表节点移动。当我们将链表的基本遍历模型结合到 fiber reconciler 中,我们可以得到不同类型链表的核心遍历模型:

  • 父子节点构成的链表

    ts
    while (workInProgress !== null) {
      workInProgress = workInProgress.child
    }
    
  • 兄弟节点构成的链表

    ts
    while (workInProgress !== null) {
      workInProgress = workInProgress.sibling
    }
    
  • 子父节点构成的链表

    ts
    while (workInProgress !== null) {
      workInProgress = workInProgress.child
    }
    

双缓冲策略

在讨论 react 内部的链表模型细节之前,我们首先需要了解一下被 react 称为上层抽象思想的 “双缓冲” 策略。

该策略是一种广泛应用于视图更新的场景。比如,现在有一幅画需要大面积修改,常规的思路是直接在原画上进行修改。但是双缓冲策略是一次性修改,首先对于所有的修改点建立的一个副本,当所有修改点完成后一次性更新到视图中。

比如,在前端领域中,当我们在不依赖外部框架,仅通过 Web DOM API 操作 DOM nodes 时,当需要大面积修改视图时,我们有两种做法:

  1. 逐个修改点修改,并且同时应用到视图中。这样带来的显著效果是,浏览器会频繁的发生 repaint 和 reflow,进而影响渲染性能。在此场景下,repaint 和 reflow 的次数与修改的次数呈正相关。

  2. 离屏修改所有的修改点,一次性更新所有修改点。这样的明显效果是,始终仅有唯一一次 repaint 和 reflow,此时修改点的个数不再影响 repaint 和 reflow,进而达到尽可能的渲染优化。

基于这一点,在 react fiber 中,至多存在两个版本的 fiber,一个是 current fiber,一个是 workInProgress fiber。current fiber 始终对应了 已经提交 到屏幕中的组件所对应的 fiber;workInProgress fiber 对应了通过触发更新创建的额 fiber node,此时该 fiber 对应的视图结果还并未提交到视图层中。所有的 workInProgress fiber 对应的视图副作用始终是在 commit phase 一次性 全量更新到视图层中。

自顶向下的父子链表遍历

当我们在 React.js 入口文件中调用 ReactDOM.render 方法时,是一切操作的起点,不难想象由 ReactDOM.render 方法触发了整个应用中的第一个 fiber node 的创建,并依据其对应的 react element 的 children 创建了接下来的直接子级 child fiber node,以此反复,直至创建整个应用的所有的 fiber node,构成最终的 react fiber tree。

一般认为每次 React 触发更新时,总是要经历三个阶段—— render phase、pre-commit phase、commit phase。其中 render phase 对应了 fiber reconciler 的整个 reconciliation 整个流程。而 commit phase 对应了将 reconciliation 的结果 一次性 地更新到屏幕中,这种将所有视图更新 一次性 更新到屏幕中的策略又被称为 “双缓冲” 策略。

本文核心在于阐述 fiber node 在 render phase 中的遍历模型。并不会过多阐述 pre-commit phase、commit phase 的实现细节。一般认为 render phase 起始于内部 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot 这两个方法调用。所有的 fiber 遍历都始于 root fiber 节点。

set.sh image

以 performSynWorkOnRoot 为示例,其核心在于 render phase 和 commit phase 的组合。render phase 始于 renderRootSync 函数,而 commit phase 始于 commitRoot 函数调用。

在 renderRootSync 中核心在于 workLoopSync 的调用。如果读者对链表结构具有一定的认识的话,那么对于 workLoopSync 函数体,我们会有一种似曾相识的感觉。

ts
// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress)
  }
}

function performUnitOfWork(unitOfWork: Fiber): void {
  // The current, flushed, state of this fiber is the alternate. Ideally
  // nothing should rely on this, but relying on it here means that we don't
  // need an additional field on the work in progress.
  const current = unitOfWork.alternate
  setCurrentDebugFiberInDEV(unitOfWork)

  let next
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork)
    next = beginWork(current, unitOfWork, subtreeRenderLanes)
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true)
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes)
  }

  resetCurrentDebugFiberInDEV()
  unitOfWork.memoizedProps = unitOfWork.pendingProps
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork)
  } else {
    workInProgress = next
  }

  ReactCurrentOwner.current = null
}

其中,我们将 performUnitOfWork 函数抽象,得到:

ts
function performUnitOfWork() {
  next = beginWork(current, UnitOfWork, subtreeRenderLanes)

  if (next === null) {
    completeUnitOfWork(unitOfWork)
  } else {
    workInProgress = next
  }
}

不难发现,以上本质上就是经典的链表遍历结构,其中 workInProgress 始终是链表遍历到的当前节点,我们调用 performUnitOfWork 来处理当前节点,并在最后赋值 workInProgress 变量,使其成为下一个待处理 fiber 节点。当 next 为 null,即到达链表尾节点时,即在当前链表中不再存在符合要求的 child fiber 时,将通过调用 completeUnitOfWork 切换 到其他类型链表,如前文所说的 子 -> 父 链表。

至此,我们将原有 performSyncWorkOnRoot 函数的职责拆分为:

  1. 一个经典的链表遍历结构;

  2. 通过 beginWork 函数遍历结构内部,通过 completeUnitOfWork 来切换当前遍历节点 workInProgress 至其他类型链表中。

    在源码中存在多处 workInProgress、current 变量,需要额外注意它们所代表的意义,不能混用。

接下来,我们深入 beginWork 来谈谈 fiber 处理的核心流程。虽然这个函数看上去很长,但是我们抽象一下其中的关键路径不难得到:

ts
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  if (current !== null) {
    // 根据 fiber 保存的组件 props 来判断是否收到了更新。
    // ...
    didReceiveUpdate = true // or false
  } else {
    // 此路径表示未收到任何更新
    didReceiveUpdate = false
  }

  workInProgress.lanes = NoLanes

  switch (
    workInProgress.tag
    // 根据 fiber.tag 执行不同的 fiber 处理路径,并返回对应的结果 fiber。
  ) {
  }
}
  1. 判断当前 render phase 执行是初始的 initial render 还是 update render,若是 update render 将通过判断 current fiber(已经渲染的组件对应的 fiber)和当前将要更新的 workInProgress fiber 间的差异(如浅比较 props)。

  2. 通过 workInProgress.tag 的值来确定该 fiber 所对应的组件类型,进而调用对应的协调算法函数来处理该 fiber。

以 HostComponent 为例,beginWork 的返回值为 updateHostComponent 的返回值。

HostComponent 表示该 fiber 对应真实的 DOM 节点,而非 functional/class component。

在 updateHostComponent 中,将调用 reconcileChildren 协调后代节点,并返回 workInProgress.child 作为调用结果。

这里即表示当 workInProgress.tag 为 HostComponent 时,updateHostComponent 将返回 workInProgress.child,即 beginWork 在此时将返回 workInProgress.child,即表示 performUnitOfWork 中的 next 变量为 workInProgress.child。

diff
function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress)
  }
}

function performUnitOfWork() {
-  next = beginWork(current, UnitOfWork, subtreeRenderLanes)
+  next = unitOfWork.child

  if (next === null) {
    completeUnitOfWork(unitOfWork)
  } else {
    workInProgress = next
  }
}

至此,就是一个完整的 父 -> 子 链表的遍历模型。回到整个 react fiber tree 中,react 内部始终首先遍历 第一个 子节点。

对子集进行协调

另外,之前忽略了 reconcileChildren 的调用执行,该函数的主要职责在于根据 current 为当前 workInProgress.child 提供数据。即通过协调的方式来确定接下来即将被渲染的 workInProgress.child 应该是怎样的。这个过程通俗来说又被称为 diff

初始渲染时

当不存在 current fiber 时,表示当前是 initial render,此时将通过 mountChildFibers 来创建当前遍历 fiber 的子 fiber。

ts
// react-reconciler/src/ReactChildFiber.js
export const mountChildFibers = ChildReconciler(false)

function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  // If this is a fresh new component that hasn't been rendered yet, we
  // won't update its child set by applying minimal side-effects. Instead,
  // we will add them all to the child before it gets rendered. That means
  // we can optimize this reconciliation pass by not tracking side-effects.
  workInProgress.child = mountChildFibers(
    workInProgress,
    null,
    nextChildren,
    renderLanes
  )
}

在上文中,当初次渲染时,因为组件是全新的还未被渲染,那么我们将直接把所有 child 加入到子集中而无需协调。

更新渲染时

当存在 current fiber 时,表示当前是 update render,那么通过 reconcileChildFibers 更新 workInProgress.child。

ts
// react-reconciler/src/ReactChildFiber.js
export const reconcileChildFibers = ChildReconciler(true)

function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  // If the current child is the same as the work in progress, it means that
  // we haven't yet started any work on these children. Therefore, we use
  // the clone algorithm to create a copy of all the current children.
  // If we had any progressed work already, that is invalid at this point so
  // let's throw it out.
  workInProgress.child = reconcileChildFibers(
    workInProgress,
    current.child,
    nextChildren,
    renderLanes
  )
}

树的完整转换在常规情况下至少具有 O(n3) 的时间复杂度,而这样的运行时开销是高性能 web 应用所无法接受的,那么 react 基于两点假设实现了 O(n) 时间复杂度的协调算法。

from reactjs.org: React implements a heuristic O(n) algorithm based on two assumptions:

  • Two elements of different types will produce different trees.
  • The developer can hint at which child elements may be stable across different renders with a key prop.
  • 不同类型的 react element 始终产生不同的 render tree;

  • 开发者可通过给子项指定 key props 来确保子项的稳定(作为子项的唯一标识)。

结合 ChildrenReconciler 函数体抽象,不论是初次渲染还是更新渲染,对于 workInProgress.child 来说,它的值始终对应 ChildrenReconciler 中 reconcileChildFibers(与前文的 export const reconcileChildFibers 不同) 的返回值。

ts
function ChildReconciler(shouldTrackSideEffects: boolean) {
  // ...
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes
  ): Fiber | null {
    // ...
  }
  return reconcileChildFibers
}

为了保持文章主旨清晰,ChildReconciler 细节部分在此不做拓展。我们这里只需要知道,所有的 workInProgress.child 由 ChildReconciler 产生。

我们结合前文对 父->子 链表遍历的分析,因为 reconcileChildren 主体是对 mountChildFibers/reconcileChildFibers(都是由 ChildReconciler 产生)的调用并赋值给 workInProgress.child,而 reconcileChildren 是在 updateXComponent 系列函数中被调用,那么进一步会影响到诸如 updateXComponent 之类函数(如 updateHostComponent)的调用。而 updateXComponent 始终是作为 beginWork 的返回值,那么进而会影响到 beginWork 的返回值,进而影响到 performUnitOfWork 中的 next 赋值,进而影响到 workLoopSync 的 workInProgress 遍历。至此我们可以将子集的协调 ChildReconciler 和 父->子 链表遍历关联起来了。

自底向上的子父链表回溯遍历

与自顶向下的父子链表遍历相反,当 react fiber tree 遍历到叶子节点时,需要回溯到父级节点,以此反复直至根 fiber node。

同样依据 performUnitOfWork 可见,当到达 child fiber linked list 的尾节点时,将调用与 beginWork 相对应的 completeUnitOfWork。

ts
function performUnitOfWork(unitOfWork: Fiber) {
  // ...

  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork)
  } else {
    workInProgress = next
  }

  // ...
}

于 completeWork 之内

与 beginWork 结构体相似的是,completeUnitOfWork 的返回值一同样依赖 completeWork 函数的调用,但不是唯一的依赖。

ts
next = completeWork(current, completedWork, subtreeRenderLanes)

查看 completeWork 函数体发现,同样存在与 beginWork 相似的函数体结构:

ts
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  const newProps = workInProgress.pendingProps

  switch (workInProgress.tag) {
    // 根据 tag 值判断对应的组件类型,进而调用不同的 handler,并将该 handler 的返回值作为 completeWork 的返回值。
    case HostComponent:
      // ...
      return null
  }
}

以 HostComponent 为示例,同样考虑 initial render 和 update render。

ts
case HostComponent:
  if (current !== null && workInProgress.stateNode !== null) {
    // update render path
  } else {
    // initial render path
  }

根据 fiber structure 我们可知 stateNode 保存了对 react instance 或 DOM node 的引用,进而我们得出以上抽象执行路径。

completeWork 初始渲染时

初始渲染时,核心执行路径如下:

ts
const instance = createInstance(
  type,
  newProps,
  rootContainerInstance,
  currentHostContext,
  workInProgress
)
appendAllChildren(instance, workInProgress, false, false)
workInProgress.stateNode = instance

if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
  markUpdate(workInProgress)
}

首先通过 createInstance 函数调用得到根据当前 workInProgress fiber 的 type 类型创建当前 fiber 的组件实例。这里涉及 react renderer 接口——React Host Config,那么在浏览器,native 等端,均有不同的 createInstance 函数体实现。以 react DOM renderer 为示例:

ts
export type Instance = Element
function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object
): Instance {
  let parentNamespace: string
  const domElement = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace
  )
  precacheFiberNode(internalInstanceHandle, domElement)
  updateFiberProps(domElement, props)
  return domElement
}

在浏览器环境下 createInstance 始终返回 fiber 对应的 DOM element,并将其作为此时的 react instance。

在创建 fiber 对应的 instance,通过对应的 precacheFiberNode 和 updateFiberProps 在 instance 的特定常量字段上保存当前 workInProgress fiber 和 fiber props 的引用。

ts
createInstance(/* ... */ workInProgress)

//...
precacheFiberNode(internalInstanceHandle, domElement)
updateFiberProps(domElement, props)

// ...
function precacheFiberNode(
  hostInst: Fiber,
  node: Instance | TextInstance | SuspenseInstance | ReactScopeInstance,
): void {
  (node: any)[internalInstanceKey] = hostInst;
}
export function updateFiberProps(
  node: Instance | TextInstance | SuspenseInstance,
  props: Props,
): void {
  (node: any)[internalPropsKey] = props;
}

以上 internalInstanceKey 和 internalPropsKey 均为 runtime 常量

在完成 react instance 创建后,将交由 appendAllChildren 完成接下来的流程。根据 react reconciler 的阐述和 浏览器 renderer 的配置 可以得到以下 appendAllChildren 的实现

ts
function appendAllChildren(
  parent: Instance,
  workInProgress: Fiber,
  needsVisibilityToggle: boolean,
  isHidden: boolean
) {
  // We only have the top Fiber that was created but we need recurse down its
  // children to find all the terminal nodes.
  let node = workInProgress.child
  while (node !== null) {
    // ...
  }
}

根据 appendAllChildren 在 react dom renderer 中的实现,处理其可能的子节点,并在 img element 上新增 img DOM 属性(attribute)。之后会将产出的 html element 通过 fiber.stateNode 进行存储。

ts
workInProgress.stateNode = instance

并将此时的 workInProgress fiber 的 flags 标记中新增 Update。

最终进入:

ts
if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
  markUpdate(workInProgress)
}

此时根据 finalizeInitialChildren 函数体可知,其主要职责是在之前创建的 react instance 上设置 DOM 属性,并返回是否应该 focus 当前 DOM element 的结果作为 finalizeInitialChildren 的结果。

当需要聚焦(focus)当前 DOM element 时,将调用 markUpdate 给当前 workInProgress fiber 标记中新增 Update 标记。

ts
workInProgress.flags |= Update

并在之后 completeWork 将恒定返回 null 值。

completeWork 更新渲染时

那么在分析完成初始渲染时的执行路径后,回到 HostComponent 在 completeWork 中的执行路径,根据 current 变量和 workInProgress.stateNode 标识得到 update render 时的执行路径,如下:

ts
if (current !== null && workInProgress.stateNode != null) {
  updateHostComponent(
    current,
    workInProgress,
    type,
    newProps,
    rootContainerInstance
  )

  if (current.ref !== workInProgress.ref) {
    markRef(workInProgress)
  }
}

return null
  1. 根据已经渲染你到屏幕中的 current fiber 和状态更新而产生的 workInProgress fiber 更新 DOM element;

  2. 浅比较 current fiber 和 workInProgress fiber 的 ref 引用是否一致,当发生差异时,通过位操作在 workInProgress fiber 中添加 Ref 标记。

并且与初次渲染一样,completeWork 恒定返回 null 值。

于 completeUnitOfWork 中,completeWork 之外

结合上文对 completeWork 的返回值分析,以 fiber tag 为 HostComponent 为示例:

diff
function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork
  // ...
-  next = completeWork(current, completedWork, subtreeRenderLanes)
+  next = null
}

此时我们根据 completeWork 的返回值可知,此处的 next 变量为 null 值。那么此时将不会走更新全局 workInProgress 变量的逻辑,即以下 completeUnitOfWork 中的逻辑将跳过:

在 v17.0.2 的 completeWork 中,仅当 fiber.tag 为 Suspense Component 相关的 tag 时才会有可能返回非 null 值。

ts
function completeUnitOfWork(unitOfWork: Fiber): void {
  // ...
  if (next !== null) {
    // Completing this fiber spawned new work. Work on that next.
    workInProgress = next
    return
  }
  // ...
}

接下来将进入与父级 fiber 构建 effect list,此处 effect list 构建与遍历模型无强关联,故跳过。完成 effect list 构建之后会进行 关键 的链表切换:

  1. 首先尝试切换到兄弟节点,当存在兄弟节点时,赋值全局遍历变量 workInProgress,进而在 workLoop 中开始遍历兄弟节点及其所有后代节点。

    ts
    function completeUnitOfWork(unitOfWork: Fiber): void {
      do {
        // ...
        const siblingFiber = completedWork.sibling
        if (siblingFiber !== null) {
          // If there is more work to do in this returnFiber, do that next.
          workInProgress = siblingFiber
          return
        }
        // ...
      } while (completedWork !== null)
      // we've reached the root.
      // ...
    }
    

    此时赋值全局变量 workInProgress 后,将回到 performUnitOfWork 的执行,随着 performUnitOfWork 执行完成,进而回到 workLoopSync 函数的 while 循环体,进而开始下一轮的 performUnitOfWork 调用,此时将传入上文代码的 siblingFiber 变量所指对象,并开始执行 next = beginWork(siblingFiber)

  2. 当无法再继续遍历兄弟节点时,那么 开始回溯至父节点

    ts
    function completeUnitOfWork(unitOfWork: Fiber): void {
      do {
        // ...
        // Otherwise, return to the parent
        completedWork = returnFiber
        // Update the next thing we're working on in case something throws.
        workInProgress = completedWork
        // ...
      } while (completedWork !== null)
      // we've reached the root.
      // ...
    }
    

    同样的,最终会回到 workLoopSync 中的 while 循环体,并进行下一次 performUnitOfWork(returnFiber) 调用。

通过以上两种场景,我们可以进行抽象得到 fiber 链表切换模型

ts
function completeUnitOfWork(unitOfWork: Fiber): void {
  const completedWork = unitOfWork

  do {
    const returnFiber = completedWork.return

    // 切换至兄弟节点所在链表
    const siblingFiber = completedWork.sibling
    if (siblingFiber !== null) {
      workInProgress = siblingFiber
      return
    }

    // 切换至子父链表
    workInProgress = completedWork = returnFiber
  } while (completedWork !== null)
  // we've reached the root.
  // ...
}

对于上文兄弟链表的切换,我们很容易理解,当全局遍历 workInProgress 切换到兄弟 fiber 时,将 退出 do ... while 遍历,并回到最初的 performUnitOfWork 调用,此时继续进行基于 siblingFiber 的 父->子 链表遍历。

子->父 链表的遍历,我们认为是 父->子 遍历的回溯遍历,此时因为基于以下语句赋值:

ts
// ...
workInProgress = completedWork = returnFiber

并结合 do ... while 语句的判断条件得知,子->父 遍历整个都是基于 completeUnitOfWork 中的 do ... while 循环来实现

为什么要在此循环中,而不是基于 performUnitOfWork 实现节点回溯?

因为回溯节点是基于构建 effect list,那么这是回溯到根节点的必要性。再者因为之前的父节点已经在 beginWork 中得到过处理,那么在回溯时可以避免再次通过 beginWork 重复处理此时的祖先节点,故仅在 do ... while loop 中处理父级回溯。

结合 completeWork

至此我们基本理解了 fiber 回溯的核心路径,那么结合前文对 completeWork 的 分析,completeWork 核心在于包含了对当前 fiber 所对应的 component 的处理方式 updateXComponent,在借助于 do...while 回溯至父节点时,父级 fiber 始终会进入 completeWork 逻辑并通过 updateXComponent 处理当前 fiber。

下文以 HostComponent 为示例:

当初始渲染时,在所有回溯路径上的 fiber 最终都会经过 completeWork 中的 appendAllChildren 方法实现连接后代更新后所产生的 DOM element。最终当回溯到 root fiber 时,将得到一个离屏渲染的 DOM fragment。并在 commit phase 实现 一次性 添加到当前视图中(“双缓冲” 策略的实际应用)。

当更新渲染时,在所有回溯路径上的 fiber 最终将通过 updateHostComponent 实现浅比较 props,标记更新等。最终实现在 commit phase 借助于此的标记更新 DOM node。

完整的抽象遍历模型

结合前文 performUnitOfWork 和 workLoopSync 相关函数,我们可以得到整个基于多种链表的树遍历模型:

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

function performUnitOfWork(unitOfWork: Fiber) {
  // ...

  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork)
  } else {
    workInProgress = next
  }

  // ...
}

function completeUnitOfWork(unitOfWork: Fiber): void {
  const completedWork = unitOfWork
  const returnFiber = completedWork.return

  // 切换至兄弟节点所在链表
  const siblingFiber = completedWork.sibling
  if (siblingFiber !== null) {
    workInProgress = siblingFiber
    return
  }

  // 切换至子父链表
  workInProgress = completedWork = returnFiber
}
  1. beginWork 始终返回当前 fiber 的 child fiber 节点;

  2. 当 beginWork 不再返回 fiber node 时,表示到达叶子节点,进入 completeUnitOfWork 执行;

  3. 由 completeUnitOfWork 处理叶子节点,并:

    1. 尝试切入 fiber.sibling 节点,并回到第一步,此时是基于 performUnitOfWork 整个流程遍历兄弟 fiber 所对应的父子链表;

    2. 当无法进入 fiber.sibling 时,尝试回溯至父级节点。此时父级回溯(其必要性在于每个节点与其父级构建 effect list)为避免重复处理祖先节点,故基于 completeUnitOfWork 中的 do ... while loop 实现回溯至根节点 而不是 performUnitOfWork 整个流程。

react fiber 遍历模型实践

在完成了上述对于 fiber 遍历模型的基本阐述后,我们结合示例来实践一下上文的思考路径。我们假定以下示例:

tsx
const App: React.FC = function App() {
  return (
    <div className="App">
      <img src={logo} className="App-logo" alt="logo" />
      <p>
        <span>{count}</span>
      </p>
      <button onClick={() => setCount(count + 1)}>increment</button>
    </div>
  )
}

// ...
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

当我们对 react fiber tree 有了基本认识后,我们不难得到以下遍历顺序:

set.sh image