Fiber 架构和双缓存
React 16 开始引入了 Fiber 架构,它的主要目的是为了解决 React 15 存在的一些问题,比如递归调用栈过深导致的卡顿、无法中断渲染、无法优先级更新等。即将递归的无法中断的更新重构为异步的可中断更新
Fiber 的含义
Fiber 的三层含义:
- 作为架构来说:
- React 15 的
Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler - React 16 的
Reconciler基于Fiber 节点实现,被称为Fiber Reconciler
- React 15 的
- 作为静态的数据结构来说:每个
Fiber 节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的 DOM 节点等信息 - 作为动态的工作单元来说:每个
Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)
Fiber Reconciler 的主要作用
- 能够把可中断的任务切片处理
- 能够调整优先级,重置并复用任务
- 能够在父元素与子元素之间交错处理,以支持 React 中的布局
- 能够在
render()中返回多个元素 - 更好地支持错误边界
Fiber reconciler | React 旧版官方文档
Fiber 的数据结构
Fiber 上主要有 DOM、Fiber 树、状态数据、副作用四种标识
function FiberNode(
tag: WorkTag, // /react-reconciler/src/ReactWorkTags.js
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
/*! --------------- 作为静态数据结构 --------------- */
this.tag = tag // Fiber 对应组件的类型
this.key = key // key
this.elementType = null // 大部分情况同 type,某些情况不同,比如 FunctionComponent 使用 React.memo 包裹
this.type = null // FunctionComponent 指函数本身;ClassComponent 指 class;HostComponent 指 DOM 节点的tagName
this.stateNode = null // Fiber 对应的真实DOM节点
/*! --------------- 作为 Fiber 架构 --------------- */
this.return = null // 指向父级 Fiber 节点
this.child = null // 指向第一个子 Fiber 节点
this.sibling = null // 指向下一个兄弟 Fiber 节点
this.index = 0
this.ref = null
/*! -------------- 作为动态的工作单元 --------------- */
// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps
this.memoizedProps = null
this.updateQueue = null
this.memoizedState = null
this.dependencies = null
this.mode = mode
// Effects 副作用相关
this.flags = NoFlags
this.subtreeFlags = NoFlags
this.deletions = null
// 调度优先级相关
this.lanes = NoLanes
this.childLanes = NoLanes
// 指向该 Fiber 节点对应的双缓存 Fiber 节点
this.alternate = null
}Fiber 双缓存
双缓存
当我们用 canvas 绘制动画时,每一帧绘制前都会调用 ctx.clearRect 清除上一帧的画面,如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。
为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。
这种在内存中构建并直接替换的技术叫做双缓存
React 使用“双缓存”来完成 Fiber 树的构建与替换——对应着 DOM 树的创建与更新
Fiber 双缓存的构建
在 React 中最多会同时存在两棵 Fiber 树
- 当前屏幕上显示内容对应的 Fiber 树叫做
current Fiber 树 - 正在内存中构建的 Fiber 树叫做
workInProgress Fiber 树
React 应用的根节点通过使 current 指针在不同 Fiber 树 的 rootFiber 间切换来完成 current Fiber 树 指向的切换
当 workInProgress Fiber 树 构建完成交给 Renderer 渲染在页面上后,React 会将应用根节点的 current 指针指向 workInProgress Fiber 树,此时 workInProgress Fiber 树 就变为 current Fiber 树
每次状态更新都会产生新的 workInProgress Fiber 树,通过 current 与 workInProgress 的替换,完成 DOM 更新
TIP
current Fiber 树中的Fiber 节点被称为current fiberworkInProgress Fiber 树中的Fiber 节点被称为workInProgress fibercurrent Fiber 树中的Fiber 节点都有alternate属性指向workInProgress Fiber 树中对应的Fiber 节点
currentFiber.alternate === workInProgressFiber
workInProgressFiber.alternate === currentFibermount 阶段
以下面的代码为 🌰
function App() {
const [num, add] = useState(0)
return <p onClick={() => add(num + 1)}>{num}</p>
}
ReactDOM.render(<App />, document.getElementById('root'))- 首次执行
ReactDOM.render时会创建fiberRootNode(源码中叫fiberRoot)和rootFiber
fiberRootNode是整个应用的根节点rootFiber是<App/>所在组件树的根节点
为什么要区分 fiberRootNode 与 rootFiber
因为在一个 React 应用中我们可以多次调用 ReactDOM.render 来渲染不同的组件树,这时它们会拥有不同的 rootFiber。但是整个应用的根节点只有一个那就是 fiberRootNode
这时 fiberRootNode 的 current 指针会指向当前页面上已渲染内容对应 Fiber 树(即 current Fiber 树)

fiberRootNode.current = rootFiber由于是首屏渲染,页面中还没有挂载任何 DOM,所以
fiberRootNode.current指向的rootFiber是没有任何子 Fiber 节点的(即current Fiber 树为空)接下来进入
render 阶段,根据组件返回的 JSX 在内存中依次创建Fiber 节点并连接在一起构建Fiber 树,其被称为workInProgress Fiber 树(下图中右侧为内存中构建的树,左侧为页面显示的树)
在构建 workInProgress Fiber 树 时会尝试复用 current Fiber 树 中已有的 Fiber 节点 内的属性,在首屏渲染时只有 rootFiber 存在对应的 current fiber(即 rootFiber.alternate)

- 图中右侧已构建完的
workInProgress Fiber 树会在commit 阶段渲染到页面
此时 DOM 更新为右侧树对应的界面。fiberRootNode 的 current 指针指向 workInProgress Fiber 树 使其变更为current Fiber 树(即下图所示)

update 阶段
- 当我们点击
p 节点触发状态改变时,会开启一次新的render 阶段并构建一棵新的workInProgress Fiber 树

和 mount 时一样,workInProgress fiber 的创建会复用 current Fiber 树 中对应的节点数据
决定是否复用的过程就是 Diff 算法
workInProgress Fiber 树在render 阶段完成构建后进入commit 阶段渲染到页面上。在渲染完毕后workInProgress Fiber 树变更为current Fiber 树

相关资料
