什么是协调
Reconciliation 协调,也可以翻译成调和,他主要负责找出哪些组件发生了变化,鼎鼎大名的 diff 算法就是在这个阶段使用的。协调阶段即可以生成 Fiber,也可以更新 Fiber,协调阶段执行完,虚拟 DOM 生成或更新完,并会在内存中生成好虚拟 DOM 对应的真实 DOM。
Reconciler 工作的阶段被称为 render 阶段。因为在该阶段会调用组件的 render 方法。
三种模式
看源码之前我们先要知道 React 启动的三种模式,有助于我们对源码脉络的理解。
在 React 16 之后,React 都有 3 种启动方式:
- legacy 模式,
ReactDOM.render(<App />, rootNode),这是我们用得最多的模式传统模式,内部采用同步模式,这个模式下调度基本上不起作用; - blocking 模式,
ReactDOM.createBlockingRoot(rootNode).render(<App />),迁移到 concurrent 模式的一个过渡形态,实际用得不多; - concurrent 模式,
ReactDOM.createRoot(rootNode).render(<App />),预计在 React 18 会正式发布,并发模式下回开启所有新功能。
源码分析
以 React.render 为例,协调阶段始于 performSyncWorkOnRoot 函数(前面还有一些调用,如何调用到这里的先暂时不管),从名字上就可以看出,这是从 根 Fiber 开始执行同步任务。
整体调用过程如下图所示:
beginWork
performSyncWorkOnRoot 函数中有一个关键调用是 renderRootSync ,然后他又调用了 prepareFreshStack,作用是重置一个新的堆栈环境。然后又调用了 createWorkInProgress,创建一个 WorkInProgress,可以理解为就是创建了一个新的任务,WorkInProgress 是一个全局变量,这也是任务的开始。
// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// The fiber we're working on
let workInProgress: Fiber | null = null;
function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
// 省略一些代码
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);
// 省略一些代码
}
执行完 prepareFreshStack,又调用了workLoopSync。
// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
可以看出全局变量 workInProgress 只要不为空就会一直调用 performUnitOfWork函数,这是一个深度优先搜索的过程。
// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function performUnitOfWork(unitOfWork: Fiber): void {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentDebugFiberInDEV();
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
这个函数有一个比较重要的调用就是 beginWork,他会返回下一个 workInProgress(也是一个 Fiber 对象,是上一个的子),然后进入下一个循环, 如果没有下一个就返回空,并进入到下一个阶段。
这个可以看一下 beginWork 函数的入参:
current是unitOfWork.alternate也就是上一次渲染的FiberunitOfWork(workInProgress)是当前的正在进行处理的FibersubtreeRenderLanes(renderLanes)是赛道优先级相关的内容,暂不讨论
// 路径:packages/react-reconciler/src/ReactFiberBeginWork.new.js
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
let updateLanes = workInProgress.lanes;
// 省略一些代码
// current 不为空说明是更新,update时:如果 current 存在可能存在优化路径,可以复用 current(即上一次更新的 Fiber 节点)
if (current !== null) {
// 省略一些代码
// 获取新旧 props
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
// 若 props 更新或者上下文改变,则认为需要"接受更新"
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// 打个更新标
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) { // 优先级不够,不需要更新的情况
didReceiveUpdate = false;
switch (workInProgress.tag) {
// 省略一些代码
}
// 复用current
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// 需要更新的情况
didReceiveUpdate = true;
} else {
// 不需要更新的其他情况,这里我们的首次渲染就将执行到这一行的逻辑
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
}
// 省略一些代码
// 这坨 switch 是 beginWork 中的核心逻辑
// 首次挂载 mount时:根据 tag 不同,创建不同的子 Fiber 节点
switch (workInProgress.tag) {
// 省略一些代码
// 中间一堆 case 就是处理不同类型的节点的,比如:函数组件、Class 组件、纯文本、根节点......
case HostRoot:
// ...省略
case HostComponent:
// 最后是一段错误处理兜底
}
通过 current 是否为空可以来判断这是 mount(首次加载) 还是 update(更新)阶段。
以 HostComponent(处理一些原生的 DOM 节点) 为例,进入 case 之后会调用 updateHostComponent 函数,里面有有一个关键调用就是 reconcileChildren :
mount 时,调用 mountChildFibers,创建 子 Fiber
update 时,调用 reconcileChildFibers,他会将当前组件与该组件在上次更新时对应的Fiber节点比较(也就是俗称的Diff算法),将比较的结果生成新Fiber 节点
下面是 mountChildFibers 和 reconcileChildFibers 的定义:
// 路径:packages/react-reconciler/src/ReactChildFiber.new.js
function ChildReconciler(shouldTrackSideEffects) {
function deleteChild // ...
function placeChild // ...
function placeSingleChild // ...
function createChild // ...
// ...
// 省略一些代码
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// 省略一些代码
}
return reconcileChildFibers;
}
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
ChildReconciler 函数内部比较复杂其逻辑总结一下主要做了以下几件事情:
- 入参
shouldTrackSideEffects,字面意思就是”是否应该追踪副作用“,所以update的时候就是要追踪副作用,mount的时候就是不追踪副作用; ChildReconciler中定义了大量如 placeXXX、deleteXXX、updateXXX、reconcileXXX 等这样的函数,这些函数覆盖了对 Fiber 节点的创建、增加、删除、修改等动作,将直接或间接地被reconcileChildFibers所调用;ChildReconciler的返回值是一个名为reconcileChildFibers的函数,这个函数是一个逻辑分发器,它将根据入参的不同,执行不同的Fiber节点操作,最终返回不同的目标Fiber节点。
第一点中的副作用是什么呢?以 placeSingleChild 为例,以下是 placeSingleChild 的源码:
function placeSingleChild(newFiber: Fiber): Fiber {
// This is simpler for the single child case. We only need to do a
// placement for inserting new children.
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement;
}
return newFiber;
}
可以看出,一旦判断 shouldTrackSideEffects 为 false,那么下面所有的逻辑都不执行了,直接返回。如果执行下去会给 Fiber 节点打上一个叫 flags 的标记,像这样:
// 17 版本之前 flags 是 effectTag
newFiber.flags |= Placement;
Placement 的定义:
// 路径:packages/react-reconciler/src/ReactFiberFlags.js
export const Placement = /* */ 0b00000000000000000000010;
export const Update = /* */ 0b00000000000000000000100;
export const PlacementAndUpdate = /* */ Placement | Update;
export const Deletion = /* */ 0b00000000000000000001000;
// 省略一些代码
flags(effectTag) 记录的是副作用的类型,而所谓“副作用”,React 给出的定义是“数据获取、订阅或者修改 DOM”等动作。
使用副作用的目的是什么呢?
简而言之只用副作用的目的就是提升页面更新的效率,没有 effectTag 标记的节点就不会被修改。
在mount时只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。
假设mountChildFibers也会赋值effectTag,那么可以预见mount时整棵Fiber树所有节点都会有Placement effectTag。那么commit阶段在执行DOM操作时每个节点都会执行一次插入操作,这样大量的DOM操作是极低效的。
总结一下, beginWork 阶段就是从 根 Fiber 开始,一路深度优先遍历,生成或更新Fiber树 。
completeWork
从上面 performUnitOfWork 函数可以看出一旦 beginWork 返回 null 就会执行 completeUnitOfWork 函数:
// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function completeUnitOfWork(unitOfWork: Fiber): void {
// Attempt to complete the current unit of work, then move to the next
// sibling. If there are no more siblings, return to the parent fiber.
let completedWork = unitOfWork;
do {
// The current, flushed, state of this fiber is the alternate. Ideally
// nothing should rely on this, but relying on it here means that we don't
// need an additional field on the work in progress.
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// Check if the work completed or if something threw.
if ((completedWork.flags & Incomplete) === NoFlags) {
setCurrentDebugFiberInDEV(completedWork);
let next;
if (
!enableProfilerTimer ||
(completedWork.mode & ProfileMode) === NoMode
) {
next = completeWork(current, completedWork, subtreeRenderLanes);
} else {
startProfilerTimer(completedWork);
next = completeWork(current, completedWork, subtreeRenderLanes);
// Update render duration assuming we didn't error.
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
}
resetCurrentDebugFiberInDEV();
if (next !== null) {
// Completing this fiber spawned new work. Work on that next.
workInProgress = next;
return;
}
} else {
// 省略一些代码
if (next !== null) {
next.flags &= HostEffectMask;
workInProgress = next;
return;
}
// 省略一些代码
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
workInProgress = siblingFiber;
return;
}
// Otherwise, return to the parent
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null);
// We've reached the root.
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
}
从上面代码可以看出在 completeWork 阶段,当前 Fiber 处理完成之后,如果有兄弟节点的话,会去找兄弟节点 siblingFiber,否则,会返回其父节点也就是 returnFiber,直到 根Fiber。
兄弟节点肯定是没有走过 beginWork 的,所以,会跳回到 beginWork 函数,对其兄弟节点再进行一次深度优先遍历。
在 completeUnitOfWork 函数中还有一些对 Fiber 的 flags、subtreeFlags、deletions 等属性做操作的逻辑,标志着当前节点需不需要更新或者删除等,这个要是为了在后面提交的时候可以直接使用当前阶段的成果。
值得关注的是这个里面有个关键调用就是 completeWork 函数:
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
bubbleProperties(workInProgress);
return null;
case ClassComponent: {
// 省略一些代码
case HostRoot: {
// 省略一些代码
}
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
if (!newProps) {
invariant(
workInProgress.stateNode !== null,
'We must have new props for new mounts. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
// This can happen when we abort work.
bubbleProperties(workInProgress);
return null;
}
const currentHostContext = getHostContext();
const wasHydrated = popHydrationState(workInProgress);
if (wasHydrated) {
if (
prepareToHydrateHostInstance(
workInProgress,
rootContainerInstance,
currentHostContext,
)
) {
// If changes to the hydrated node need to be applied at the
// commit-phase we mark this as such.
markUpdate(workInProgress);
}
} else {
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
// If there is a ref on a host node we need to schedule a callback
markRef(workInProgress);
}
}
bubbleProperties(workInProgress);
return null;
}
case HostText: {
// 省略一些代码
}
}
// 省略一些代码
}
completeWork 函数主要是一堆 switch case 语句构成,会根据 workInProgress 节点的 tag 属性不同,进入到不同的 DOM 节点创建、处理逻辑,比如:HostComponent 就是指的原生 DOM 元素类型。
completeWork 内部有三个关键动作:
- 创建 DOM 节点(createInstance);
- 将 DOM 节点插入到 DOM 树中(appendAllChildren);
- 为 DOM 节点设置属性(FinalizeInitialChildren)。
另外,创建好的 DOM 节点会被赋值到对应的 Fiber 的 stateNode 属性。
综上所述,在 completeWork 阶段主要做的事情就是负责处理 Fiber 节点到 DOM 节点的映射。
实例解读
import React from 'react';
import ReactDOM from 'react-dom';
function App() {
return (
<div className="App">
<div className="container">
<h1>Hello, Eagle</h1>
<p>text1</p>
<p>text2</p>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
下图是以上组件协调阶段的执行过程,可以帮助我们更好地理解 beginWork 的深度优先搜索过程,并且可以看到 beginWork 与 completeWork 其实是交替执行的。
另外,父子 Fiber 之间使用 return、child 属性连接,兄弟 Fiber 之间使用 sibling 属性进行连接。
至此,协调阶段全部工作完成。在 performSyncWorkOnRoot 函数中 fiberRootNode 被传递给 commitRoot 方法,开启 commit阶段 工作流程,也就是 Renderer 的工作。