Fiber既是 react 定义的一种数据结构,也是我们常说的虚拟 DOM
由一个个Fiber节点,构成的树就是Fiber Tree

为什么要设计 Fiber 数据结构

React15 在 render 阶段的 reconcile 是不可打断的,这会在进行大量节点的 reconcile 时可能产生卡顿,因为浏览器所有的时间都交给了 js 执行,而JS线程GUI渲染线程互斥。

为此 React16 之后就有了 scheduler 进行时间片的调度,给每个 task(工作单元)一定的时间,如果在这个时间内没执行完,也要交出执行权给浏览器进行绘制和重排。
而异步可中断的更新需要一定的数据结构,在内存中来保存工作单元的信息,所以出现了 Fiber 数据结构。

Fiber 的作用

  1. 工作单元,任务分解 :Fiber 最重要的功能就是作为工作单元,保存原生节点或者组件节点对应信息(包括优先级)。这些节点通过指针的形式,构成 Fiber 树
  2. 增量渲染:通过 jsx 对象和 current Fiber 的对比,生成最小的差异补丁(Flags),应用到真实节点上
  3. 根据优先级暂停、继续、排列优先级:Fiber 节点上保存了优先级,能通过不同节点优先级的对比,达到任务的暂停、继续、排列优先级等能力,也为上层实现批量更新、Suspense 提供了基础
  4. 保存状态:因为 Fiber 能保存状态和更新的信息,所以就能实现函数组件的状态更新,也就是 hooks

Fiber 长什么样

Fiber 节点的构建函数如下:

  1. //ReactFiber.old.js
  2. function FiberNode(
  3. tag: WorkTag,
  4. pendingProps: mixed,
  5. key: null | string,
  6. mode: TypeOfMode,
  7. ) {
  8. //作为静态的数据结构 保存节点的信息
  9. this.tag = tag; //对应组件的类型
  10. this.key = key; //key 属性
  11. this.elementType = null;//元素类型
  12. this.type = null; //func 或者 class
  13. this.stateNode = null; //真实 dom 节点
  14. //作为 fiber 树架构 连接成 fiber 树
  15. this.return = null; //指向父节点
  16. this.child = null; //指向child
  17. this.sibling = null; //指向兄弟节点
  18. this.index = 0;
  19. this.ref = null;
  20. //用作为工作单元 来计算state
  21. this.pendingProps = pendingProps;
  22. this.memoizedProps = null;
  23. this.updateQueue = null;
  24. this.memoizedState = null;
  25. this.dependencies = null;
  26. this.mode = mode;
  27. //effect相关
  28. this.effectTag = NoEffect;
  29. this.nextEffect = null;
  30. this.firstEffect = null;
  31. this.lastEffect = null;
  32. //优先级相关的属性
  33. this.lanes = NoLanes;
  34. this.childLanes = NoLanes;
  35. //current和workInProgress的指针
  36. this.alternate = null;
  37. }

举个例子

  1. function App() {
  2. return (
  3. <div>
  4. i am
  5. <span>KaSong</span>
  6. </div>
  7. )
  8. }

Fiber 架构(SDOM) - 图1

Fiber 之双缓存

什么是双缓存

当我们用 canvas 绘制动画,每一帧绘制前都会调用 ctx.clearRect 清除上一帧的画面。

如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。

为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。

这种在内存中构建并直接替换的技术叫做双缓存
React 使用“双缓存”来完成 Fiber 树的构建与替换——对应着DOM树的创建与更新。

双缓存 Fiber 树

在 React 中最多会同时存在两棵 Fiber 树

  • 在屏幕上显示的 DOM 元素对应的 Fiber 树称为 current Fiber 树
    • current Fiber 树 中的 Fiber 节点 被称为 current fiber
  • 正在内存中构建的 Fiber 树称为 workInProgress Fiber 树
    • workInProgress Fiber 树 中的 Fiber 节点 被称为 workInProgress fiber

两棵树的 Fiber 节点,通过alternate 属性连接

  1. currentFiber.alternate === workInProgressFiber;
  2. workInProgressFiber.alternate === currentFiber;

workInProgress Fiber 树 构建完成交给 Renderer 渲染在页面上后,把根节点(fiberRootNode)current 指针指向 workInProgress Fiber 树,此时 workInProgress Fiber树 就变为 current Fiber 树

每次状态更新都会产生新的 workInProgress Fiber 树,通过 current 与 workInProgress 的替换,完成 DOM 更新。

接下来我们以具体例子讲解 mount 时、update 时的构建/替换流程。

Mount 时

  1. function App() {
  2. const [num, add] = useState(0);
  3. return (
  4. <p onClick={() => add(num + 1)}>{num}</p>
  5. )
  6. }
  7. ReactDOM.render(<App/>, document.getElementById('root'));
  1. 首次执行 ReactDOM.render 会创建 fiberRootNode(源码中叫fiberRoot)和rootFiber
    1. 其中fiberRootNode是整个应用的根节点
    2. rootFiber<App/>所在组件树的根节点。

      由于是首屏渲染,页面中还没有挂载任何 DOM,所以fiberRootNode.current指向的rootFiber没有任何子Fiber节点(即 current Fiber 树为空)

Fiber 架构(SDOM) - 图2

  1. 接下来进入 render 阶段,根据组件返回的 JSX 在内存中依次创建 Fiber节点 并连接在一起构建 Fiber树,被称为 workInProgress Fiber 树。(下图中右侧为内存中构建的树,左侧为页面显示的树)

    在构建 workInProgress Fiber树 时会尝试复用 current Fiber树 中已有的 Fiber 节点 内的属性。 在首屏渲染时只有 rootFiber 存在对应的 current fiber(即 rootFiber.alternate

Fiber 架构(SDOM) - 图3

  1. 图中右侧已构建完的 workInProgress Fiber 树 在 commit 阶段 渲染到页面。

    此时 DOM 更新为右侧树对应的样子。fiberRootNodecurrent 指针指向 workInProgress Fiber树 使其变为 current Fiber 树。

Fiber 架构(SDOM) - 图4

Update 时

  1. 接下来我们点击 p 节点触发状态改变,这会开启一次新的 render 阶段并构建一棵新的 workInProgress Fiber 树

和 mount 时一样,workInProgress fiber 的创建可以复用 current Fiber 树 对应的节点数据。

这个决定是否复用的过程就是 Diff 算法

Fiber 架构(SDOM) - 图5

  1. workInProgress Fiber 树 在 render 阶段完成构建后,进入 commit 阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树 变为 current Fiber 树

Fiber 架构(SDOM) - 图6