React Fiber架构

1,777 阅读5分钟

React设计理念React架构 中我们知道,在 v15 版本 Reconciler 采用递归的方式更新虚拟 DOM,这会导致什么问题呢?

由于递归过程是不能中断的,如果组件树的层级很深,递归更新时间超过了一帧,用户交互就会卡顿,

为了解决这个问题,v16递归的无法中断的更新重构为异步的可中断更新

由于曾经用于递归的虚拟 DOM 数据结构已经无法满足需要,于是全新的 Fiber 架构应运而生。

那到底什么是 Fiber ?Fiber 只是一个架构吗?为什么说 Fiber 同时也作为静态的数据结构和动态的工作单元?

什么是 Fiber

React 团队的核心成员 Andrew Clark 在 2016 年的一次演讲 What's Next for React — ReactNext 2016 中第一次提到 Fiber,

这次演讲后来被整理为一篇介绍 React Fiber Architecture ,Fiber 作为 React 新版本的一种核心架构被正式提出。

A description of React's new core algorithm, React Fiber

在实现上,Fiber 对应了 DOM 树中的一个节点,我们可以从以下三个角度解读 Fiber:架构、数据结构和工作单元

Fiber Architecture

作为架构而言,v15Reconciler 采用递归的方式实现,数据保存在递归调用栈中,叫做 Stack Reconciler

v16Reconciler 基于 Fiber 节点实现,被称为 Fiber Reconciler,支持可中断的异步更新,任务支持切片,

Fiber Data Structure

Fiber 也是一种数据结构,我们可以从源码找到 Fiber节点的属性定义(关键的属性已经添加了注释,可以结合注释来看),

function FiberNode(
  this: $FlowFixMe,
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode
) {
  // Fiber 对应组件的类型 Function/Class/Host...
  this.tag = tag;
  // Diffing 需要的 key 属性
  this.key = key;
  // 大部分情况同 type,某些情况不同,比如 FunctionComponent 使用 React.memo 包裹
  this.elementType = null;
  // 对于 FunctionComponent,指函数本身,对于 ClassComponent,指 class,对于 HostComponent,指 DOM 节点的 tagName
  this.type = null;
  // Fiber 对应的真实 DOM 节点
  this.stateNode = null;

  // 指向父级 Fiber 节点
  this.return = null;
  // 指向子 Fiber 节点
  this.child = null;
  // 指向兄弟 Fiber 节点
  this.sibling = null;
  this.index = 0;

  this.ref = null;
  this.refCleanup = null;

  // 保存本次更新的状态改变相关信息
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  // 保存本次更新会造成的DOM操作
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;

  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该 Fiber 在另一次更新时对应的 Fiber
  this.alternate = null;
}

Fiber 作为静态的数据结构,保存了组件的 tagkeytypestateNode 等相关信息。

Fiber Work Unit

Fiber 同时也是一个动态的工作单元,从 FiberNode 的定义中我们发现,

Fiber 保存了本次更新的状态改变相关信息会造成的 DOM 操作(副作用 effect)以及调度优先级相关的信息

Fiber 之所以可以作为 Work Unit,还与其工作原理有关,在正式介绍之前,我们先来了解一个 DOM 更新的技术:双缓存

双缓存

双缓存(Double Buffering)是一个广泛应用于网络传输图形渲染内存读取优化等场景的技术,

我们以大家比较熟悉的渲染场景为例,假设我们需要用 canvas 绘制一个动画,然后显示到屏幕上,通常我们会在显示缓存区进行绘制,绘制每一帧前都会调用 ctx.clearRect 清除上一帧的画面,如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏,

image

为了解决这个问题,我们可以在自定义缓存区中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,这样就可以省去了两帧替换间的计算时间,避免白屏到出现画面的闪烁。

这种在内存中构建并直接替换的技术就是双缓存

React Fiber 就是利用了双缓存技术来完成 “Fiber树” 的创建和替换,从而提高性能,具体是怎么实现的呢?

Alternate

React 中最多会同时存在两颗 Fiber 树,每次状态更新都会产生新的 workInProgress Fiber

  • currentFiber,当前屏幕上显示内容对应的 Fiber 树
  • workInProgressFiber,正在内存中构建的 Fiber 树

它们之间通过 alternate 属性连接,

currentFiber.alternate === workInProgressFiber; // true
workInProgressFiber.alternate === currentFiber; // true

React 应用的根节点通过使 current 指针在不同 Fiber 树的 rootFiber 间切换来完成 currentFiber 树指向的切换,

我们可以以组件 mount/update 的流程为例,了解 Fiber 树的构建和更新过程,考虑如下例子,

function App() {
  const [num, add] = useState(0);
  return (
    <p onClick={() => add(num + 1)}>{num}</p>
  )
}

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

mount

首次执行 ReactDOM.render 会创建 fiberRootrootFiber ,其中,

  • fiberRoot 是整个应用的根节点
  • rootFiber<App/> 所在组件树的根节点

此时,fiberRootcurrent 指针指向当前的 Fiber 树,

image

接着会进入 render 阶段,React 会解析组件返回的 JSX 并在内存中依次创建 Fiber 节点连接成 Fiber 树,内存中完成构建的 Fiber 树叫做 workInProgress Fiber 树,这个过程中 React 会尝试复用 current Fiber 树中已有的 Fiber 节点内的属性,

image

然后是 commit 阶段,右侧已经构建完的 Fiber 树会替换掉当前的 Fiber 树,渲染到页面,

image

update

我们来接着讨论更新阶段,假设我们点击 p 节点触发状态改变,num 的值从 0 变为 1

每一次更新 React 都会开启一次新的 render 阶段并构建一棵新的 workInProgress Fiber 树,

mount 时一样,workInProgressFiber 的创建会尝试复用 currentFiber 节点,这个过程就是 Diffing 算法,

image

render 阶段完成后接着会进入 commit 阶段,workInProgressFiber 替换 currentFiber 完成渲染,

image

小结

Fiber 作为 React 新版本的核心架构,在实现可中断的异步更新中至关重要,

通过上述介绍,我们了解到 Fiber 不止作为架构,同时也是静态的数据结构动态的工作单元

Fiber 架构的核心是双缓存技术,其创建和更新的过程伴随着 DOM 的更新。

参考链接

写在最后

本文首发于我的 博客,才疏学浅,难免有错误,文章有误之处还望不吝指正!

如果有疑问或者发现错误,可以在评论区进行提问和勘误,

如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。