Reconciler
Reconciler
在 React v16+ 中, Reconciler 与 Renderer 不再是交替工作。
- 当 Scheduler 将任务交给 Reconciler 后,Reconciler 会为变化的虚拟 DOM 打上代表增/删/更新的标记。
- 整个 Scheduler 与 Reconciler 的工作都在内存中进行,只有当所有组件都完成 Reconciler 的工作,才会统一交给 Renderer。
Reconciler render 阶段
Reconciler render 阶段开始于 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot 函数的调用。
performSyncWorkOnRoot(Legacy Sync模式):执行同步渲染任务。performConcurrentWorkOnRoot(Concurrent模式):执行并发渲染任务。在函数中,会调用shouldYield函数判断是否需要中断遍历,使浏览器有时间渲染。如果当前浏览器帧没有剩余时间,则会终止循环,直到浏览器有空闲时间后再继续遍历。
performSyncWorkOnRoot 与 performConcurrentWorkOnRoot 函数
// react\packages\react-reconciler\src\ReactFiberWorkLoop.old.js
// ensureRootIsScheduled 用于 rootFiber 的任务调度。一个 root 只有一个任务在执行,每次更新和任务退出前都会调用此函数。
// 1. 计算新任务的过期时间、优先级
// 2. 无新任务,退出调度
// 3. 有历史任务:
// 3.1 新旧任务的优先级相同,继续执行旧任务,(新任务会在旧任务执行完成之后的同步刷新钩子中执行)
// 3.2 新旧任务的优先级不相同,取消旧任务
// 4. 根据不同的 Priority (优先级) 执行不同的调度(scheduleSyncCallback(同步) 或 scheduleCallback(异步)), 最后将返回值设置到 fiberRoot.callbackNode
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// ......
// Schedule a new callback.
let newCallbackNode
if (newCallbackPriority === SyncLane) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
if (root.tag === LegacyRoot) {
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root))
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root))
}
// ......
} else {
let schedulerPriorityLevel
// lanesToEventPriority : 将 lane 优先级转换为 event 优先级
// 以区间的形式,根据传入的 lane 返回对应的 event 优先级。比如,传入的优先级不大于 Discrete 优先级,就返回 Discrete 优先级,以此类推
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
// DiscreteEventPriority 离散事件优先级。click、keydown、focusin等,事件的触发不是连续,可以做到快速响应
schedulerPriorityLevel = ImmediateSchedulerPriority
break
case ContinuousEventPriority:
// ContinuousEventPriority 连续事件优先级。drag、scroll、mouseover等,事件的是连续触发的,快速响应可能会阻塞渲染,优先级较离散事件低
schedulerPriorityLevel = UserBlockingSchedulerPriority
break
case DefaultEventPriority:
// DefaultEventPriority 默认的事件优先级
schedulerPriorityLevel = NormalSchedulerPriority
break
case IdleEventPriority:
// IdleEventPriority 空闲的优先级
schedulerPriorityLevel = IdleSchedulerPriority
break
default:
schedulerPriorityLevel = NormalSchedulerPriority
break
}
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
)
}
}
// 执行同步渲染任务
function performSyncWorkOnRoot(root) {
let exitStatus = renderRootSync(root, lanes)
// ... 省略部分代码
commitRoot(
root,
workInProgressRootRecoverableErrors,
workInProgressTransitions
)
// Before exiting, make sure there's a callback scheduled for the next
// pending level.
// 退出前再次检测, 是否还有其他更新, 是否需要发起新调度
ensureRootIsScheduled(root, now())
return null
}
// 执行并发渲染任务
function performConcurrentWorkOnRoot(root, didTimeout) {
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout)
// 退出状态
// 默认情况下,并发更新总是使用时间切片
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes) // 执行可中断更新
: renderRootSync(root, lanes) // 循环调用 workLoopSync 进行同步更新
// ......
// 退出前再次检测, 是否还有其他更新, 是否需要发起新调度
ensureRootIsScheduled(root, now())
// 下一个要渲染的任务,递归调用 performConcurrentWorkOnRoot自身 继续执行调度
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
// 渲染被阻断, 返回一个新的 performConcurrentWorkOnRoot 函数, 等待下一次调用
return performConcurrentWorkOnRoot.bind(null, root)
}
return null
}
// 执行同步更新
function renderRootSync(root: FiberRoot, lanes: Lanes) {
// ......
do {
try {
workLoopSync()
break
} catch (thrownValue) {
handleError(root, thrownValue)
}
} while (true)
// ......
}
// 执行可中断更新
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// ......
do {
try {
workLoopConcurrent()
break
} catch (thrownValue) {
handleError(root, thrownValue)
}
} while (true)
// ......
}
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress)
}
}
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress)
}
}
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
}在 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot 函数中,最终会调用 performUnitOfWork(workInProgress) 函数。
workInProgress表示当前已创建的workInProgress fiber。performUnitOfWork(workInProgress)函数会创建下一个Fiber 节点并赋值给workInProgress,并将workInProgress与已创建的Fiber 节点连接起来构成 Fiber 树。
Fiber Reconciler 通过遍历的方式实现可中断的递归,对于 performUnitOfWork 的工作可以分为两部分:“递”和“归”。“递”和“归”阶段会交错执行,直到“归”到 rootFiber,此时 Reconciler render 阶段完成。
“递”阶段
从
rootFiber开始向下深度优先遍历,为遍历到的每个Fiber 节点调用beginWork方法。该方法根据传入的Fiber 节点创建子 Fiber 节点,并将两个Fiber 节点连接起来。“归”阶段
当遍历到叶子节点(即:没有子组件的组件)时,会进入“归”阶段,会调用
completeWork方法处理Fiber 节点。- 当某个
Fiber 节点执行完completeWork,如果其存在兄弟 Fiber 节点(即 :fiber.sibling !== null),会进入其兄弟 Fiber的“递”阶段。 - 如果不存在
兄弟 Fiber,会进入父级 Fiber的“归”阶段。
- 当某个
performUnitOfWork 函数
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
}
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 {
// This fiber did not complete because something threw. Pop values off
// the stack without entering the complete phase. If this is a boundary,
// capture values if possible.
const next = unwindWork(current, completedWork, subtreeRenderLanes)
// Because this fiber did not complete, don't reset its lanes.
if (next !== null) {
// If completing this work spawned new work, do that next. We'll come
// back here again.
// Since we're restarting, remove anything that is not a host effect
// from the effect tag.
next.flags &= HostEffectMask
workInProgress = next
return
}
if (
enableProfilerTimer &&
(completedWork.mode & ProfileMode) !== NoMode
) {
// Record the render duration for the fiber that errored.
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false)
// Include the time spent working on failed children before continuing.
let actualDuration = completedWork.actualDuration
let child = completedWork.child
while (child !== null) {
actualDuration += child.actualDuration
child = child.sibling
}
completedWork.actualDuration = actualDuration
}
if (returnFiber !== null) {
// Mark the parent fiber as incomplete and clear its subtree flags.
returnFiber.flags |= Incomplete
returnFiber.subtreeFlags = NoFlags
returnFiber.deletions = null
} else {
// We've unwound all the way to the root.
workInProgressRootExitStatus = RootDidNotComplete
workInProgress = null
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 === RootInProgress) {
workInProgressRootExitStatus = RootCompleted
}
}“递”阶段 - beginWork

beginWork 函数
let didReceiveUpdate: boolean = false
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
if (current !== null) {
/* 更新流程 */
// current 树上上一次渲染后的 props
const oldProps = current.memoizedProps
// workInProgress 树上这一次更新的 props
const newProps = workInProgress.pendingProps
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
// Force a re-render if the implementation changed due to hot reload:
(__DEV__ ? workInProgress.type !== current.type : false)
) {
// If props or context changed, mark the fiber as having performed work.
// This may be unset if the props are determined to be equal later (memo).
didReceiveUpdate = true
} else {
// Neither props nor legacy context changes. Check if there's a pending
// update or context change.
// props 和 context 没有发生变化,检查是否更新来自自身或者 context 改变
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes
)
if (
!hasScheduledUpdateOrContext &&
// If this is the second pass of an error or suspense boundary, there
// may not be work scheduled on `current`, so we check for this flag.
(workInProgress.flags & DidCapture) === NoFlags
) {
// No pending updates or context. Bail out now.
didReceiveUpdate = false
// attemptEarlyBailoutIfNoScheduledUpdate 函数会处理部分 Context 逻辑
// 在函数内部,调用了 bailoutOnAlreadyFinishedWork
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes
)
}
if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
// This is a special case that only exists for legacy mode.
// See https://github.com/facebook/react/pull/19216.
didReceiveUpdate = true
} else {
// An update was scheduled on this fiber, but there are no new props
// nor legacy context. Set this to false. If an update queue or context
// consumer produces a changed value, it will set this to true. Otherwise,
// the component will assume the children have not changed and bail out.
didReceiveUpdate = false
}
}
} else {
didReceiveUpdate = false
if (getIsHydrating() && isForkedChild(workInProgress)) {
// Check if this child belongs to a list of muliple children in
// its parent.
//
// In a true multi-threaded implementation, we would render children on
// parallel threads. This would represent the beginning of a new render
// thread for this subtree.
//
// We only use this for id generation during hydration, which is why the
// logic is located in this special branch.
const slotIndex = workInProgress.index
const numberOfForks = getForksAtLevel(workInProgress)
pushTreeId(workInProgress, numberOfForks, slotIndex)
}
}
// Before entering the begin phase, clear pending update priority.
// TODO: This assumes that we're about to evaluate the component and process
// the update queue. However, there's an exception: SimpleMemoComponent
// sometimes bails out later in the begin phase. This indicates that we should
// move this assignment out of the common path and into each branch.
workInProgress.lanes = NoLanes
switch (workInProgress.tag) {
case IndeterminateComponent: {
// ... 省略部分代码
}
case LazyComponent: {
// ... 省略部分代码
}
case FunctionComponent: {
// ... 省略部分代码
}
case ClassComponent: {
// ... 省略部分代码
}
// ... 省略部分 case 相关代码
}
}beginWork(current, workInProgress, renderLanes) 函数的作用是传入当前 Fiber 节点,创建 子 Fiber 节点 。在该函数中:
update(即:current !== null) 时如果
current存在(即current !== null),在满足一定条件时可以复用current节点,就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child。满足如下情况时,
didReceiveUpdate === false,即可以直接复用前一次更新的子 Fiber,不需要新建子 FiberoldProps === newProps && workInProgress.type === current.type,即props与fiber.type不变!includesSomeLane(renderLanes, updateLanes),即当前Fiber 节点优先级不够
注:
didReceiveUpdate标识当前更新是否来源于父级的更新,自身并没有更新。比如:更新父组件,其子组件也会跟着更新,这个情况下didReceiveUpdate = true。mount(即:current == null) 时组件
mount时,除fiberRoot以外,满足如下情况,会根据fiber.tag不同,创建不同类型的子Fiber 节点。- 首次渲染,不存在当前组件对应
Fiber 节点在上一次更新时的Fiber 节点(即current === null) - 当不满足优化路径时
- 首次渲染,不存在当前组件对应
reconcileChildren(current, workInProgress, nextChildren, renderLanes)对于常见的组件类型,如(
FunctionComponent/ClassComponent/HostComponent),创建新的子 Fiber 节点,会进入reconcileChildren方法。该方法会生成新的
子 Fiber 节点并赋值给workInProgress.child,作为本次beginWork返回值,并作为下次performUnitOfWork执行时workInProgress的传参。- 对于
mount的组件,创建新的子 Fiber 节点 - 对于
update的组件,将当前组件与该组件在上次更新时对应的Fiber 节点比较(即 Diff 算法),将比较的结果生成新Fiber 节点
reconcileChildren 函数
export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderLanes: Lanes ) { if (current === null) { // If this is a fresh new component that hasn't been rendered yet, we // won't update its child set by applying minimal side-effects. Instead, // we will add them all to the child before it gets rendered. That means // we can optimize this reconciliation pass by not tracking side-effects. workInProgress.child = mountChildFibers( workInProgress, null, nextChildren, renderLanes ) } else { // If the current child is the same as the work in progress, it means that // we haven't yet started any work on these children. Therefore, we use // the clone algorithm to create a copy of all the current children. // If we had any progressed work already, that is invalid at this point so // let's throw it out. workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderLanes ) } }- 对于
EffectTagrender阶段是在内存中进行,结束后会通知Renderer执行 DOM 操作,执行 DOM 操作的具体类型保存在fiber.effectTag中。// 通过二进制表示 effectTag,可以方便的使用位操作为 fiber.effectTag 赋值多个 effect // DOM 需要插入到页面中 export const Placement = /* */ 0b00000000000000000000000010 // DOM 需要更新 export const Update = /* */ 0b00000000000000000000000100 // DOM 需要删除 export const Deletion = /* */ 0b00000000000000000000001000 // ......如果要通知
Renderer将Fiber 节点对应的 DOM 节点插入页面中,需要满足两个条件:fiber.stateNode存在,即Fiber 节点中保存了对应的 DOM 节点。(fiber.effectTag & Placement) !== 0,即Fiber 节点存在Placement effectTag。
mount时,fiber.stateNode === null,且在reconcileChildren中调用的mountChildFibers不会为Fiber 节点赋值effectTag。在首屏渲染中:fiber.stateNode会在completeWork中创建。- 假设
mountChildFibers也会赋值effectTag,可以预见mount时整棵Fiber树所有节点都会有Placement effectTag。在mount时,只有rootFiber会赋值Placement effectTag,在commit阶段只会执行一次插入操作。
“归”阶段 - completeWork

completeWork(current, workInProgress, renderLanes) 针对不同 fiber.tag 的 Fiber 节点 执行对应操作。主要分析页面渲染所必须的 HostComponent(即,原生 DOM 组件对应的 Fiber 节点)
completeWork 函数
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
const newProps = workInProgress.pendingProps
// Note: This intentionally doesn't check if we're hydrating because comparing
// to the current tree provider fiber is just as fast and less error-prone.
// Ideally we would have a special version of the work loop only
// for hydration.
popTreeContext(workInProgress)
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: {
// ... 省略部分代码
return null
}
case HostRoot: {
// ... 省略部分代码
updateHostContainer(current, workInProgress)
// ... 省略部分代码
return null
}
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) {
if (workInProgress.stateNode === null) {
throw new Error(
'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()
// TODO: Move createInstance to beginWork and keep it on a context
// "stack" as the parent. Then append children as we go in beginWork
// or completeWork depending on whether we want to add them top->down or
// bottom->up. Top->down is faster in IE11.
const wasHydrated = popHydrationState(workInProgress)
if (wasHydrated) {
// TODO: Move this and createInstance step into the beginPhase
// to consolidate.
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
// Certain renderers require commit-time effects for initial mount.
// (eg DOM renderer supports auto-focus for certain elements).
// Make sure such renderers get scheduled for later work.
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: {
// ... 省略部分代码
return null
}
// ... 省略部分 case 代码
}
}update时update时(即current !== null && workInProgress.stateNode != null),Fiber 节点已经存在对应 DOM 节点,所以不需要生成 DOM 节点,要做的主要是处理props。onClick、onChange等回调函数的注册- 处理
style prop - 处理
DANGEROUSLY_SET_INNER_HTML prop - 处理
children prop - ......
主要的逻辑是调用
updateHostComponent方法。在该方法中,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。workInProgress.updateQueue = (updatePayload: any)updatePayload为数组形式,他的偶数索引的值为变化的prop key,奇数索引的值为变化的prop value。mount时mount时的主要逻辑包括:- 为
Fiber 节点生成对应的 DOM 节点。通过调用createInstance方法为Fiber 节点创建 DOM 节点,创建好的 DOM 节点会赋值给fiber.stateNode。 - 将子孙 DOM 节点插入刚生成的 DOM 节点中。
completeWork属于“归”阶段调用的函数,每次调用appendAllChildren方法时,都会将已生成的子孙 DOM 节点插入当前生成的 DOM 节点下。当“归”到 rootFiber 时,已经有一个构建好的离屏 DOM 树。 - 与 update 逻辑中的
updateHostComponent类似的处理 props 的过程。通过调用finalizeInitialChildren方法,初始化 DOM 对象的事件监听器和内部属性。
- 为
EffectListEffectList作为 DOM 操作的依据,commit阶段需要找到所有有EffectTag的Fiber 节点并依次执行EffectTag对应操作。在
completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在EffectTag的Fiber 节点会被保存在EffectList的单向链表中。EffectList中第一个Fiber 节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。在“归”阶段,所有有
EffectTag的Fiber 节点都会被追加在EffectList中,最终形成一条以rootFiber.firstEffect为起点的单向链表。在commit阶段只需要遍历EffectList就能执行所有effect了。commitRoot(root)在
performSyncWorkOnRoot函数中fiberRoot被传递给commitRoot方法,开启commit阶段工作流程。commitRoot 函数
function commitRoot( root: FiberRoot, recoverableErrors: null | Array<CapturedValue<mixed>>, transitions: Array<Transition> | null ) { // TODO: This no longer makes any sense. We already wrap the mutation and // layout phases. Should be able to remove. const previousUpdateLanePriority = getCurrentUpdatePriority() const prevTransition = ReactCurrentBatchConfig.transition try { ReactCurrentBatchConfig.transition = null setCurrentUpdatePriority(DiscreteEventPriority) commitRootImpl( root, recoverableErrors, transitions, previousUpdateLanePriority ) } finally { ReactCurrentBatchConfig.transition = prevTransition setCurrentUpdatePriority(previousUpdateLanePriority) } return null }
Reconciler commit 阶段
performSyncWorkOnRoot 函数中调用 commitRoot(root) 是 commit 阶段的起点,fiberRoot 会作为入参。
Reconciler commit 阶段(即 Renderer 的工作流程)主要分为三部分:
before mutation阶段(执行 DOM 操作前)mutation阶段(执行 DOM 操作)layout阶段(执行 DOM 操作后)
在 before mutation 阶段之前和 layout 阶段之后还有一些额外工作,比如 useEffect 的触发、优先级相关的重置、ref 的绑定/解绑。
在 render 阶段,会遍历 Fiber 树,收集需要更新的地方,打不同的标志,标志会在 commit 阶段执行。
- 更新相关
- Update :组件更新标志
- Ref :处理绑定元素和组件实例
- 元素相关
- Placement :插入元素
- Update :更新元素
- ChildDeletion :删除元素
- Snapshot :元素快照
- Visibility-offscreen :新特性
- ContentReset :文本内容更新
- 更新回调,执行 effect:
- Callback-root 回调函数
- 类组件回调
- Passive-useEffect 的钩子函数
- Layout-useLayoutEffect 的钩子函数
- Insertion-useInsertionEffect 的钩子函数
before mutation
before mutation 阶段,遍历 EffectList 并调用 commitBeforeMutationEffects 函数处理。
主要调用函数调用链路:commitRoot 函数 --> commitRootImpl 函数 --> commitBeforeMutationEffects 函数 --> commitBeforeMutationEffects_begin 函数 --> commitBeforeMutationEffects_complete 函数 --> commitBeforeMutationEffectsOnFiber 函数
处理 DOM 节点渲染/删除后的
autoFocus、blur逻辑。调用
getSnapshotBeforeUpdate生命周期钩子。commitBeforeMutationEffectsOnFiber函数,主要用与处理Snapshot,获取 DOM 更新前的快照信息,对于ClassComponent会调用实例的getSnapShotBeforeUpdate函数。从 React v16 开始,
componentWillXXX钩子函数前增加了UNSAFE_前缀,是因为 Stack Reconciler 重构为 Fiber Reconciler 后,render阶段的任务可能中断/重新开始,对应的组件在render阶段的生命周期钩子(即componentWillXXX)可能触发多次。为此,React 提供了替代的生命周期钩子getSnapshotBeforeUpdate。getSnapshotBeforeUpdate是在commit阶段内的before mutation阶段调用的,由于commit阶段是同步的,所以不会遇到多次调用的问题。调度 useEffect
在
flushPassiveEffects方法内部会从全局变量rootWithPendingPassiveEffects获取EffectList。EffectList中保存了需要执行副作用的Fiber 节点。其中副作用包括:插入 DOM 节点(Placement)、更新 DOM 节点(Update)、删除 DOM 节点(Deletion)。除此外,当一个FunctionComponent含有useEffect或useLayoutEffect,对应的Fiber 节点也会被赋值effectTag。整个
useEffect异步调用分为三步:before mutation阶段之前,在scheduleCallback中调度flushPassiveEffects注册回调函数layout阶段之后,将EffectList赋值给rootWithPendingPassiveEffects全局变量,回调函数执行的时候才有值scheduleCallback触发flushPassiveEffects,flushPassiveEffects内部遍历rootWithPendingPassiveEffects
与
componentDidMount、componentDidUpdate不同的是,在浏览器完成布局与绘制之后,传给useEffect的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。可见,useEffect异步执行的原因主要是防止同步执行时阻塞浏览器渲染。flushPassiveEffects 函数
export function flushPassiveEffects(): boolean { // Returns whether passive effects were flushed. // TODO: Combine this check with the one in flushPassiveEFfectsImpl. We should // probably just combine the two functions. I believe they were only separate // in the first place because we used to wrap it with // `Scheduler.runWithPriority`, which accepts a function. But now we track the // priority within React itself, so we can mutate the variable directly. if (rootWithPendingPassiveEffects !== null) { // Cache the root since rootWithPendingPassiveEffects is cleared in // flushPassiveEffectsImpl const root = rootWithPendingPassiveEffects // Cache and clear the remaining lanes flag; it must be reset since this // method can be called from various places, not always from commitRoot // where the remaining lanes are known const remainingLanes = pendingPassiveEffectsRemainingLanes pendingPassiveEffectsRemainingLanes = NoLanes const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes) const priority = lowerEventPriority(DefaultEventPriority, renderPriority) const prevTransition = ReactCurrentBatchConfig.transition const previousPriority = getCurrentUpdatePriority() try { ReactCurrentBatchConfig.transition = null setCurrentUpdatePriority(priority) return flushPassiveEffectsImpl() } finally { setCurrentUpdatePriority(previousPriority) ReactCurrentBatchConfig.transition = prevTransition // Once passive effects have run for the tree - giving components a // chance to retain cache instances they use - release the pooled // cache at the root (if there is one) releaseRootPooledCache(root, remainingLanes) } } return false }
commitBeforeMutationEffects 函数
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber
) {
focusedInstanceHandle = prepareForCommit(root.containerInfo)
nextEffect = firstChild
commitBeforeMutationEffects_begin()
// We no longer need to track the active instance fiber
const shouldFire = shouldFireAfterActiveInstanceBlur
shouldFireAfterActiveInstanceBlur = false
focusedInstanceHandle = null
return shouldFire
}
function commitBeforeMutationEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect
// This phase is only used for beforeActiveInstanceBlur.
// Let's skip the whole loop if it's off.
if (enableCreateEventHandleAPI) {
// TODO: Should wrap this in flags check, too, as optimization
const deletions = fiber.deletions
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const deletion = deletions[i]
commitBeforeMutationEffectsDeletion(deletion)
}
}
}
const child = fiber.child
if (
(fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
child !== null
) {
child.return = fiber
nextEffect = child
} else {
commitBeforeMutationEffects_complete()
}
}
}
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect
setCurrentDebugFiberInDEV(fiber)
try {
commitBeforeMutationEffectsOnFiber(fiber)
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error)
}
resetCurrentDebugFiberInDEV()
const sibling = fiber.sibling
if (sibling !== null) {
sibling.return = fiber.return
nextEffect = sibling
return
}
nextEffect = fiber.return
}
}
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate
const flags = finishedWork.flags
if (enableCreateEventHandleAPI) {
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
// Check to see if the focused element was inside of a hidden (Suspense) subtree.
// TODO: Move this out of the hot path using a dedicated effect tag.
if (
finishedWork.tag === SuspenseComponent &&
isSuspenseBoundaryBeingHidden(current, finishedWork) &&
doesFiberContain(finishedWork, focusedInstanceHandle)
) {
shouldFireAfterActiveInstanceBlur = true
beforeActiveInstanceBlur(finishedWork)
}
}
}
if ((flags & Snapshot) !== NoFlags) {
setCurrentDebugFiberInDEV(finishedWork)
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
break
}
case ClassComponent: {
if (current !== null) {
const prevProps = current.memoizedProps
const prevState = current.memoizedState
const instance = finishedWork.stateNode
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState
)
instance.__reactInternalSnapshotBeforeUpdate = snapshot
}
break
}
case HostRoot: {
if (supportsMutation) {
const root = finishedWork.stateNode
clearContainer(root.containerInfo)
}
break
}
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// Nothing to do for these component types
break
default: {
throw new Error(
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.'
)
}
}
resetCurrentDebugFiberInDEV()
}
}mutation
mutation 阶段,遍历 EffectList ,执行函数。
主要调用函数调用链路:commitRoot 函数 --> commitRootImpl 函数 --> commitMutationEffects 函数 --> commitMutationEffectsOnFiber 函数
在 commitMutationEffectsOnFiber(finishedWork.tag, root, lanes) 函数中,根据 finishedWork.tag 执行不同的操作,对新增元素,更新元素,删除元素进行真实的 DOM 操作。
详情
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes
) {
const current = finishedWork.alternate
const flags = finishedWork.flags
// The effect flag should be checked *after* we refine the type of fiber,
// because the fiber tag is more specific. An exception is any flag related
// to reconcilation, because those can be set on all fiber types.
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// ... 省略部分代码
return
}
case ClassComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes)
commitReconciliationEffects(finishedWork)
if (flags & Ref) {
if (current !== null) {
// 清空 ref
safelyDetachRef(current, current.return)
}
}
return
}
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes)
commitReconciliationEffects(finishedWork)
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return)
}
}
if (supportsMutation) {
// TODO: ContentReset gets cleared by the children during the commit
// phase. This is a refactor hazard because it means we must read
// flags the flags after `commitReconciliationEffects` has already run;
// the order matters. We should refactor so that ContentReset does not
// rely on mutating the flag during commit. Like by setting a flag
// during the render phase instead.
if (finishedWork.flags & ContentReset) {
const instance: Instance = finishedWork.stateNode
try {
resetTextContent(instance)
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error)
}
}
if (flags & Update) {
const instance: Instance = finishedWork.stateNode
if (instance != null) {
// Commit the work prepared earlier.
const newProps = finishedWork.memoizedProps
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldProps = current !== null ? current.memoizedProps : newProps
const type = finishedWork.type
// TODO: Type the updateQueue to be specific to host components.
const updatePayload: null | UpdatePayload =
(finishedWork.updateQueue: any)
finishedWork.updateQueue = null
if (updatePayload !== null) {
try {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork
)
} catch (error) {
captureCommitPhaseError(
finishedWork,
finishedWork.return,
error
)
}
}
}
}
}
return
}
case HostText: {
// ... 省略部分代码
return
}
case HostRoot: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes)
commitReconciliationEffects(finishedWork)
if (flags & Update) {
if (supportsMutation && supportsHydration) {
if (current !== null) {
const prevRootState: RootState = current.memoizedState
if (prevRootState.isDehydrated) {
try {
commitHydratedContainer(root.containerInfo)
} catch (error) {
captureCommitPhaseError(
finishedWork,
finishedWork.return,
error
)
}
}
}
}
if (supportsPersistence) {
const containerInfo = root.containerInfo
const pendingChildren = root.pendingChildren
try {
replaceContainerChildren(containerInfo, pendingChildren)
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error)
}
}
}
return
}
// ... 省略部分 case 代码
default: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes)
commitReconciliationEffects(finishedWork)
return
}
}
}layout
layout 阶段,在 DOM 渲染完成(mutation 阶段完成)后执行的,触发的生命周期钩子和 Hook 可以直接访问到已经改变后的 DOM,即可以参与 DOM layout 的阶段。
主要调用函数调用链路:commitRoot 函数 --> commitRootImpl 函数 --> commitLayoutEffects 函数 --> commitLayoutEffects_begin 函数 --> commitLayoutMountEffects_complete 函数 --> commitLayoutEffectOnFiber 函数
在 commitLayoutEffectOnFiber(finishedRoot, current, finishedWork, committedLanes) 方法中:
根据
fiber.tag对不同类型的节点分别处理。- 对于
ClassComponent,通过current === null区分是mount与update,调用componentDidMount或componentDidUpdate。 - 对于
FunctionComponent及相关类型(指特殊处理后的FunctionComponent,比如ForwardRef、React.memo包裹的FunctionComponent),调用useLayoutEffect Hook回调函数,调度useEffect的销毁与回调函数 - ......
- 对于
通过调用
commitAttachRef函数获取 DOM 实例,更新 ref 。commitAttachRef 函数
function commitAttachRef(finishedWork: Fiber) { const ref = finishedWork.ref if (ref !== null) { const instance = finishedWork.stateNode let instanceToUse switch (finishedWork.tag) { case HostComponent: instanceToUse = getPublicInstance(instance) break default: instanceToUse = instance } // Moved outside to ensure DCE works with this flag if (enableScopeAPI && finishedWork.tag === ScopeComponent) { instanceToUse = instance } if (typeof ref === 'function') { let retVal if ( enableProfilerTimer && enableProfilerCommitHooks && finishedWork.mode & ProfileMode ) { try { startLayoutEffectTimer() retVal = ref(instanceToUse) } finally { recordLayoutEffectDuration(finishedWork) } } else { retVal = ref(instanceToUse) } } else { ref.current = instanceToUse } } }
commitLayoutEffectOnFiber 函数
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes
): void {
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
if (
!enableSuspenseLayoutEffectSemantics ||
!offscreenSubtreeWasHidden
) {
// At this point layout effects have already been destroyed (during mutation phase).
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer()
commitHookEffectListMount(
HookLayout | HookHasEffect,
finishedWork
)
} finally {
recordLayoutEffectDuration(finishedWork)
}
} else {
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork)
}
}
break
}
case ClassComponent: {
const instance = finishedWork.stateNode
if (finishedWork.flags & Update) {
if (!offscreenSubtreeWasHidden) {
if (current === null) {
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer()
instance.componentDidMount()
} finally {
recordLayoutEffectDuration(finishedWork)
}
} else {
instance.componentDidMount()
}
} else {
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(
finishedWork.type,
current.memoizedProps
)
const prevState = current.memoizedState
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer()
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate
)
} finally {
recordLayoutEffectDuration(finishedWork)
}
} else {
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate
)
}
}
}
}
// TODO: I think this is now always non-null by the time it reaches the
// commit phase. Consider removing the type check.
const updateQueue: UpdateQueue<*> | null =
(finishedWork.updateQueue: any)
if (updateQueue !== null) {
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
commitUpdateQueue(finishedWork, updateQueue, instance)
}
break
}
case HostRoot: {
// TODO: I think this is now always non-null by the time it reaches the
// commit phase. Consider removing the type check.
const updateQueue: UpdateQueue<*> | null =
(finishedWork.updateQueue: any)
if (updateQueue !== null) {
let instance = null
if (finishedWork.child !== null) {
switch (finishedWork.child.tag) {
case HostComponent:
instance = getPublicInstance(finishedWork.child.stateNode)
break
case ClassComponent:
instance = finishedWork.child.stateNode
break
}
}
commitUpdateQueue(finishedWork, updateQueue, instance)
}
break
}
// ... 省略部分 case 代码
case HostComponent: {
const instance: Instance = finishedWork.stateNode
// Renderers may schedule work to be done after host components are mounted
// (eg DOM renderer may schedule auto-focus for inputs and form controls).
// These effects should only be committed when components are first mounted,
// aka when there is no current/alternate.
if (current === null && finishedWork.flags & Update) {
const type = finishedWork.type
const props = finishedWork.memoizedProps
commitMount(instance, type, props, finishedWork)
}
break
}
// ... 省略部分 case 代码
default:
throw new Error(
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.'
)
}
}
if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
if (enableScopeAPI) {
// TODO: This is a temporary solution that allowed us to transition away
// from React Flare on www.
if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
commitAttachRef(finishedWork)
}
} else {
if (finishedWork.flags & Ref) {
commitAttachRef(finishedWork)
}
}
}
}在 mutation 阶段结束后,layout 阶段开始前,会执行 root.current = finishedWork 切换 fiberRoot 指向的 current Fiber 树(workInProgress Fiber 树在 commit 阶段完成渲染后会变为 current Fiber 树)。原因如下:
componentWillUnmount会在mutation阶段执行。此时current Fiber树还指向前一次更新的Fiber树,在生命周期钩子内获取的 DOM 还是更新前的。componentDidMount和componentDidUpdate会在layout阶段执行。此时current Fiber树已经指向更新后的Fiber树,在生命周期钩子内获取的 DOM 就是更新后的。
