React Fiber

710 阅读6分钟

Fiber 是什么?

最简单直观的理解是:fiber 是 React 中的虚拟 DOM。但这样的理解不太全面,也不太恰当。

🚩 作为一个JS对象,一个 fiber 节点对应一个 React Element,保存了组件类型、props、对应DOM节点等信息,与视图每个节点一一对应,这就是虚拟DOM。但是在 React 中虚拟 DOM 一词不太恰当,React 支持跨平台,DOM只是其中之一。因此在 React 中还是直接成为 Fiber。

🚩 作为架构来说,Fiber Reconciler, 是 React16 基于 Fiber 实现的 Reconciler,是为了解决 React15 Stack Reconciler 中固有的问题,基于 Fiber Reconciler,React 可以实现:

  • 将任务切片处理,支持中断和恢复
  • 调整优先级
  • 重置并复用任务
  • 如果不需要,可以中止工作

🚩 作为时间分片的最小工作单元,每个Fiber节点保存了本次更新中该组件改变的状态 props 和要执行的工作,新增、更新或删除等

Fiber 的结构

Fiber 在 React 中是一个 FiberNode 的类,表示一个 Fiber 节点,以下分三个层面来了解一个 FiberNode 都有哪些属性

与DOM节点相关的属性

作为一个虚拟DOM节点,一个 FiberNode 通过以下属性来保存元素的相关信息:

  • tag: 对应组件的类型
  • type:对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
  • key:key 属性
  • elementType
  • stateNode: Fiber对应的真实DOM节点

与 Fiber 架构相关的属性

Fiber 是一个链表结构,通过以下三个属性来串联子结点、兄弟节点、父结点。在render阶段遍历组件树时,通过以下三个属性既能根据节点之间的父子兄弟关系串联整棵树,又能实现遍历的中断与恢复。

  • child:指向第一个子 Fiber 节点
  • sibling:指向第一个兄弟 Fiber 节点
  • return:指向父级 Fiber 节点

一个组件实例最多有两个与之对应的 Fiber 节点,一个是当前已渲染的DOM节点对应的 Fiber 节点(mount阶段没有)二是正在进行中的 workInProgress Fiber。这两个 fiber 节点互相通过 alternate 指向对方。

  • alternate

优先级相关

  • lanes
  • childLanes

与更新过程状态相关的属性

每个 Fiber 节点的更新过程,作为一个独立的任务,每次浏览器空闲时更新一个 fiber 节点,然后将控制权交回给浏览器,如果有时间,才会继续更新下一个 fiber 节点,以确保不会长时间占用线程。

由于遍历树的工作是断续进行的,就需要将状态保存在节点中,以下字段保存了本次更新的状态变更

  • pendingProps
  • memoizedProps
  • updateQueue
  • memoizedState
  • dependencies
  • mode

保存了本次更新DOM操作的相关信息

  • flags
  • subtreeFlags
  • deletions

Fiber 架构的工作原理

前置知识

双缓存Fiber树

前面说到,一个组件实例最多有两个与之对应的 Fiber 节点,对应整个应用,就是有两棵Fiber树,每个节点之间通过 alternate 互相关联。

React15中 Reconciler 和 Renderer 是交替工作的,整个过程是同步的,React16 使用了双缓存Fiber树,workInProgress 树在内存中构建完成后才一次性 commit,这个过程是异步可中断的。

mount 阶段 fiber 节点没有与之对应的 alternate,因此可以通过 alternate 是否等于 null 来判断当前是 mount 阶段还是 update 阶段。

在 update 阶段,通过对比新旧 Fiber 节点决定是否可以复用旧的节点。这个对比的过程就是 Diff 算法。等到构建出一棵完整的树,再一次性提交给浏览器进行渲染,然后替换掉 current 树成为新的 current 树。

fiberRoot 与 rootFiber

fiberRoot 是整个应用的根节点,它是始终唯一的,它有一个 current 属性,指向 rootFiber,表示 current Fiber 树

rootFiber<App /> 所在 fiber 树的根节点,它的 child 指向 App 对应的 fiber 节点。每次更新调用 ReactDOM.render 都会创建新的 rootFiber,指向 workInProgress 树。

工作过程

以下面这段代码为例:

function App() {
  const [num, add] = useState(0);
  return <div onClick={() => add(num + 1}>{num}</div>
}
ReactDOM.render(<App />, document.getElementById('app'));

mount 阶段

首次执行 ReactDOM.render 会创建 fiberRootrootFiberfiberRoot.current 指向 rootFiber,此时 rootFiber 还没有子节点。

接下来进入 render 阶段,会创建一个新的 rootFiber,然后依次遍历子组件,创建fiber节点,最终构建出一棵 fiber 树

这是异步更新的过程,从 rootFiber 开始向下深度优先遍历,为遍历到的 fiber 节点调用 beginWork 方法创建子 fiber 节点,并将这两个 fiber 节点连接起来。在update阶段时,如果节点可以复用或者优先级不够,则会直接复用旧的子fiber节点。

当遍历到叶子节点,就会调用 completeWork 处理 Fiber 节点,根据 fiber.tag 调用不同的处理逻辑,这个过程同样分 mount/update 阶段,主要处理 props、注册回调函数、判断是否需要生成 DOM 节点,update阶段会设置 fiber.effectTag 并保存到 effectList 链表中,mount阶段则是生成新的DOM节点,然后将子级DOM树插入到当前生成的DOM节点,最终构建出一棵 WIP 树的同时,就已经完成了DOM树的构建,直接将根节点插入到页面即可。

当某个 fiber 节点执行完 completeWork,如果其存在兄弟 Fiber 节点,即 fiber.sibling !== null,就会为其兄弟 fiber 节点调用 beginWork,重复上面的步骤。如果不存在兄弟节点就为父级 fiber 节点调用 completeWork。最终回到 rootFiber,就完成了 workInProgress 树的构建。

在以上过程中,每次调用 beginWork/completeWork 处理完一个 fiber 节点,将返回的节点赋值给 workInProgress,然后将控制权交回浏览器,浏览器空闲时继续处理,否则中断任务,等待下次空闲再恢复。这种将整个树的更新拆分为一个个节点进行处理的过程,被称为时间切片

构建完成的 workInProgress Fiber 树则在 commit 阶段 渲染到页面,然后会将 fiberRoot.current 指向 workInProgress 树,作为下次更新用于比较的 current 树。

image.png

update 阶段

当用户交互触发状态改变,会开启新一轮的 render 阶段并构建出一棵新的 workInProgress 树,在构建的过程中,每个节点通过 alternate 与 current Fiber 树的每个节点互相关联并对比,决定是否复用 current fiber 节点。

同样地,workInProgress 树构建完成后进入 commit 阶段,然后更新 fiberRoot.current 指向 workInProgress 树。

image.png

参考