React Fiber树的构建
前面我们介绍了 React架构,为了实现可中断的异步更新,v16 使用 Fiber 作为核心架构进行了重构,
接下来我们将深入 Fiber 树的构建,正式开始之前,我们先回顾下 Fiber 相关的一些术语,
Fiber架构下,React 架构分为三层Scheduler、Reconciler和RendererReconciler工作的阶段被称为render阶段,在该阶段会调用组件的render方法Renderer工作的阶段被称为commit阶段,在阶段会把render阶段提交的信息渲染到页面render与commit阶段统称为work,如果任务正在Scheduler内调度,则不属于work
Fiber 树的构建发生在 render 阶段,也就是 Reconciler 工作的阶段,我们知道 Fiber Reconciler 是由 Stack Reconciler 重构而来,通过遍历的方式实现可中断的递归,所以 Fiber Reconciler 的工作可以分为两个部分:“递” 和 “归”。
可中断的递和归
render 阶段开始于 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot 方法,
分别会调用 workLoopSync 和 workLoopConcurrent,这取决于本次更新是同步更新还是异步更新,
// 如果是同步更新,会调用 performSyncWorkOnRoot,performSyncWorkOnRoot 会调用 workLoopSync
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// 如果是异步更新,会调用 performConcurrentWorkOnRoot,performConcurrentWorkOnRoot 会调用 workLoopConcurrent
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
可以看到,它们都会调用 performUnitOfWork ,唯一的区别是是否判断 shouldYield,
如果浏览器没有空闲时间,shouldYield 会终止循环,直到浏览器有空闲时间后再继续遍历。
performUnitOfWork 就是递和归的起点,参数 workInProgress 就是我们之前介绍的正在构建中的 Fiber 树,
递归遍历的流程为,
- 首先进入“递”阶段,从
rootFiber开始向下进行深度优先遍历,为遍历到的每个节点调用beginWork方法 beginWork会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来- 遍历到叶子节点时进入“归”阶段
- 归”阶段会为每个节点调用
completeWork - 如果节点存在兄弟(
sibling)节点,进入兄弟节点的“递”阶段,如果不存在,进入父节点的“归”阶段 - 递”和“归”阶段会交错执行直到“归”到
rootFiber
为了方便理解,我们以下面的代码为例,
function App() {
return (
<div>
Monch
<span>Lee</span>
</div>
)
}
ReactDOM.render(<App />, document.getElementById("root"));
对应的 Fiber 树结构如下图,我们把节点的 beginWork 和 completeWork 打印出来,
render 阶段会依次执行,
1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "Monch" Fiber beginWork
5. "Monch" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork
你可能会疑惑,为什么没有 “Lee” Fiber?
原因是作为一种性能优化手段,React 会特殊处理只有单一文本子节点的 Fiber。
render 阶段的递归本质上就是 beginWork 和 completeWork,我们先从 beginWork 开始。
beginWork
函数 beginWork 的定义如下,你可以从源码的这里找到完整的定义,
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...
}
前面我们介绍,beginWork 的工作是:传入当前 Fiber 节点,创建子 Fiber 节点,
整体流程如下,

beginWork 接受三个参数,其中,
- current,当前组件对应的
Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate - workInProgress,当前组件对应的
Fiber节点 - renderLanes,当前
Fiber节点的优先级
深入函数体之前,先思考一个问题🤔 ,对于一个 Fiber 节点而言,可能存在首次渲染和更新两种情况,我们要怎么区分呢?
在 React Fiber架构 中我们介绍到,Fiber 架构是基于双缓存技术,React 中最多同时存在两颗 Fiber 树,除了 rootFiber 外,首次渲染 mount 时,是不存在当前 Fiber 节点在上一次更新时的 Fiber 节点的,
所以我们可以通过判断当前的 current Fiber 树是否为 null 来决定是 mount 还是 update,
如果 current !== null,说明是 update,React 会尝试复用节点,否则会创建节点,
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// 如果 current 存在,说明是 update,此时可能存在优化路径,React 会尝试复用 current(即上一次更新的 Fiber 节点)
if (current !== null) {
// 尝试复用 current
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
// 如果 current 不存在,说明是 mount,此时会创建子 Fiber 节点
didReceiveUpdate = false;
}
// mount时,根据 tag 不同,创建不同的子 Fiber 节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...
case FunctionComponent:
// ...
case ClassComponent:
// ...
case HostComponent:
// ...
}
}
什么情况下可以复用节点呢?
节点在满足 didReceiveUpdate === false 时 React 会尝试复用,
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = false;
}
也就是需要满足以下情况,
// props 相同,节点类型相同,节点的优先级不够
oldProps === newProps && workInProgress.type === current.type && !includesSomeLane(renderLanes, updateLanes)
这与我们前面介绍的 O(n) 的启发式 Diffing 算法的假设是相呼应的,只有类型相同的元素会继续 Diffing。
不满足复用条件时,React 会根据节点类型(tag)调用对应的方法创建子节点,最终都会调用 reconcileChildren。
reconcileChildren
reconcileChildren 会创建新的 Fiber 节点,与 beginWork 类似,通过 current === null 区分是 mount 还是 update,
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}
- 如果是
mount,创建新的子Fiber节点 - 如果是
update,调用Diffing算法,将比较的结果生成新Fiber节点
不论走哪个逻辑,最终都会生成新的子 Fiber 节点并赋值给 workInProgress.child,
这个值会作为本次 beginWork 的 返回值,及下次 performUnitOfWork 执行时 workInProgress 的 传参。
mountChildFibers 与 reconcileChildFibers 逻辑基本一致,唯一的区别是后者会为生成的 Fiber 带上 effectTag 属性,
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
function ChildReconciler(shouldTrackSideEffects) {
// ...
}
什么是 effectTag 呢?
effectTag
我们知道,render 阶段是在内存中进行的,结束后会通知 Renderer 需要执行的 DOM 操作,
这里的 effectTag 就是要执行 DOM 操作的具体类型,
export const Placement = /* */ 0b00000000000010; // 插入
export const Update = /* */ 0b00000000000100; // 更新
export const PlacementAndUpdate = /* */ 0b00000000000110; // 插入并更新
export const Deletion = /* */ 0b00000000001000; // 删除
// ...
这里通过二进制表示 effectTag,是为了方便的使用位操作为 fiber.effectTag 赋值多个 effect。
前面我们提到,mountChildFibers 不会为 Fiber 节点赋值 effectTag,那么首屏渲染是如何完成呢?
实际上,mount 时只会对 rootFiber 赋值 Placement effectTag,
这样可以保证在 commit 阶段只需要一次 DOM 插入就完成整个 DOM 树的首屏渲染,
function ChildReconciler(shouldTrackSideEffects) {
// ...
// reconcileChildFibers 会调用 placeChild 等方法为递归到的 Fiber 节点打上 effectTag
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number
): number {
if (current !== null) {
// ...
} else {
newFiber.effectTag = Placement;
return lastPlacedIndex;
}
}
// mountChildFibers 只会为 rootFiber 打上一个 Placement effectTag,作为一种优化手段,在 commit 阶段一次性完成首屏渲染
function placeSingleChild(newFiber: Fiber): Fiber {
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.effectTag = Placement;
}
return newFiber;
}
}
要插入 effectTag,需要 Fiber 节点中保存对应的 DOM 节点,也就是 stateNode 属性,它会在 completeWork 中创建。
completeWork
类似于 beginWork,completeWork 也是针对不同的 fiber.tag 调用不同的处理逻辑,

源码 completeWork 的定义如下,
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case FunctionComponent:
case Fragment:
case MemoComponent:
return null;
case ClassComponent: {
// ...
return null;
}
case HostRoot: {
// ...
updateHostContainer(workInProgress);
return null;
}
case HostComponent: {
// ...
return null;
}
}
}
我们以页面渲染必须的 HostComponent 为例,看下 completeWork 都做了哪些工作。
HostComponent
与 beginWork 类似,可以通过 current === null? 判断当前是处于 mount 还是 update 流程,
由于 update 时依赖真实的 DOM 节点,所以还要考虑 workInProgress.stateNode != null ?,
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
// 如果 current 存在并且 Fiber 节点存在对应的 DOM 节点
if (current !== null && workInProgress.stateNode != null) {
// update
} else {
// mount
}
return null;
}
mount 中存在和 update 类似的流程,这里我们先看 completeWork 的 update 流程。
update
根据前面的判断条件,update 时,Fiber 节点已经存在 DOM 节点,所以不需要创建 DOM,
update 会调用 updateHostComponent 来处理 props,包括,
updateHostComponent = function (
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container
) {
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
return;
}
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext
);
// 处理完的 props 会被赋值给 workInProgress.updateQueue,最终会在 commit 阶段被渲染在页面上
// updatePayload 为数组形式,偶数索引的值为变化的 prop key,奇数索引的值为变化的 prop value
workInProgress.updateQueue = (updatePayload: any);
if (updatePayload) {
markUpdate(workInProgress);
}
};
onClick、onChange等回调函数的注册- 处理
style prop - 处理
DANGEROUSLY_SET_INNER_HTML prop - 处理
children prop
如果是 mount ,流程会有所不同。
mount
mount 流程的主要逻辑包含三个,
- 为
Fiber节点生成对应的 DOM 节点 - 将子孙
DOM节点插入刚生成的DOM节点中 - 与
update阶段的updateHostComponent类似,处理props
const currentHostContext = getHostContext();
// 为 Fiber 创建对应 DOM 节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 将子孙 DOM 节点插入刚生成的 DOM 节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM 节点赋值给 fiber.stateNode
workInProgress.stateNode = instance;
// 处理 props
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
这里的 appendAllChildren 就是我们前面提到的离屏 DOM 树创建的关键,每次调用 appendAllChildren 都会将已生成的子孙 DOM 节点插入当前生成的 DOM 节点下,当递归到 rootFiber 时,我们就可以得到一个构建好的离屏 DOM 树,这样 commit 阶段就可以只通过一次插入 DOM 的操作将整棵 DOM 树插入到页面。
最后,所有存在 effectTag 的 Fiber 会生成 effectList。
effectList
effectList 是一个单向链表,在 completeWork 的上层函数 completeUnitOfWork 中构造,每个执行完 completeWork 且存在 effectTag 的 Fiber 节点会被保存链表中,为什么需要缓存 effectTag 呢?
这里的原因也是为了提效。
作为 DOM 操作的依据,commit 阶段需要找到所有有 effectTag 的 Fiber 节点并依次执行 effectTag 对应操作,如果不缓存,则需要再遍历一次 Fiber 树寻找 effectTag !== null 的节点,所以为了避免这种低效的操作,所有有 effectTag 的Fiber节点都会被追加在 effectList 中,最终形成一条以 rootFiber.firstEffect 为起点的单向链表,
这样,在 commit 阶段只需要遍历 effectList 就能执行所有 effect。
小结
completeWork 的工作是对上一个节点 Diffing 完成后进行一些收尾工作,会根据不同的 tag 执行 mount 或 update 操作,
如果节点需要 mount,会为其创建对应的 DOM 节点并赋值给 fiber.stateNode,这个过程会将子孙 DOM 节点依次插入生成离屏的 DOM 树,同时还会初始化 DOM 对象的事件监听器及内部属性。
如果节点需要 update,会 diff props,返回一个需要更新的属性名组成的数组然后赋值给 workInProgress.updateQueue。
最后有 effectTag 的 fiber 会生成一个单向链表 effectList 挂载到父级 fiber,并返回下一个 workInProgress。
render 阶段工作完成后,fiberRootNode 会被传递给 commitRoot方法,开启 commit 阶段的工作流程。
commitRoot(root);
参考链接
写在最后
本文首发于我的 博客,才疏学浅,难免有错误,文章有误之处还望不吝指正!
如果有疑问或者发现错误,可以在评论区进行提问和勘误,
如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
转载自:https://juejin.cn/post/7195456123183300669