Commit
Renderer and HostConfig Protocol
Renderer:
- Implementing
HostConfigprotocol. - Rendering fiber tree to real contents:
- Web: DOM node.
- Native: native UI.
- Server: SSR strings.
- Real renderer demo.
HostConfig protocol:
isPrimaryRender: true.supportsHydration: true: SSR renderer.supportsMutation: true: React DOM renderer.supportsPersistence: true: React Native renderer.- Platform timer functions:
- now.
- scheduleTimeout.
- cancelTimeout.
- Creation operations:
- createInstance.
- createTextInstance.
- UI tree operations:
- appendInitialChild.
- appendChild.
- appendChildToContainer.
- removeChildFromContainer.
- removeChild.
- clearContainer.
- Update props operations:
- finalizeInitialChildren.
- prepareUpdate.
- commitUpdate.
- commitTextUpdate.
- shouldSetTextContent.
- resetTextContent.
- Context and schedule operations:
- getRootHostContext.
- getChildHostContext.
- getPublicInstance.
- prepareForCommit.
- resetAfterCommit.
- preparePortalMount.
Commit Root
FiberRoot.finishedWork:- 副作用队列挂载在根节点上 (
finishedWork.firstEffect). - 最新 DOM 对象挂载在 HostComponent Fiber 上 (
fiber.stateNode).
- 副作用队列挂载在根节点上 (
BeforeMutationphase:- Read state of host tree right before DOM mutation.
- Process
Passive/Snapshot/Deletioneffects fiber. instance.getSnapshotBeforeUpdate.
Mutationphase.- Mutate host tree, render UI.
- Process
ContentReset/Ref/Visibility/Placement/Update/Deletion/Hydratingeffects fiber.
Layoutphase.- After DOM mutation.
- Process
Update | Callbackeffects fiber. instance.componentDidMount/componentDidUpdate(synchronous).instancecallback forsetState.useLayoutEffect(synchronous).
CommitEffectsfunctions located in ReactFiberCommitWork.
function commitRoot(root: FiberRoot, recoverableErrors: null | Array<mixed>) {
const previousUpdateLanePriority = getCurrentUpdatePriority()
const prevTransition = ReactCurrentBatchConfig.transition
try {
ReactCurrentBatchConfig.transition = null
setCurrentUpdatePriority(DiscreteEventPriority)
commitRootImpl(root, recoverableErrors, previousUpdateLanePriority)
} finally {
ReactCurrentBatchConfig.transition = prevTransition
setCurrentUpdatePriority(previousUpdateLanePriority)
}
return null
}
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<mixed>,
renderPriorityLevel: EventPriority
) {
do
flushPassiveEffects()
while (rootWithPendingPassiveEffects !== null)
flushRenderPhaseStrictModeWarningsInDEV()
if ((executionContext & (RenderContext | CommitContext)) !== NoContext)
throw new Error('Should not already be working.')
const finishedWork = root.finishedWork
const lanes = root.finishedLanes
if (finishedWork === null)
return null
// 清空 FiberRoot 对象上的属性.
root.finishedWork = null
root.finishedLanes = NoLanes
root.callbackNode = null
root.callbackPriority = NoLane
// Update first and last pending times on this root.
// New first pending time is whatever is left on root fiber.
const remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes)
if (root === workInProgressRoot) {
// We can reset these now that they are finished.
workInProgressRoot = null
workInProgress = null
workInProgressRootRenderLanes = NoLanes
}
// If there are pending passive effects, schedule a callback to process them.
// Do this as early as possible before anything else in commit phase.
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags
|| (finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true
pendingPassiveEffectsRemainingLanes = remainingLanes
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects()
return null
})
}
}
// Check if there are any effects in whole tree.
const subtreeHasEffects
= (finishedWork.subtreeFlags
& (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask))
!== NoFlags
const rootHasEffect
= (finishedWork.flags
& (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask))
!== NoFlags
if (subtreeHasEffects || rootHasEffect) {
// Store context.
const prevTransition = ReactCurrentBatchConfig.transition
const previousPriority = getCurrentUpdatePriority()
const prevExecutionContext = executionContext
ReactCurrentBatchConfig.transition = null
setCurrentUpdatePriority(DiscreteEventPriority)
executionContext |= CommitContext
// Reset this to null before calling life cycles.
ReactCurrentOwner.current = null
// `BeforeMutation` phase:
// read state of host tree right before we mutate it.
// `getSnapshotBeforeUpdate` is called.
commitBeforeMutationEffects(root, finishedWork)
// `Mutation` phase:
// mutate host tree.
commitMutationEffects(root, finishedWork, lanes)
resetAfterCommit(root.containerInfo)
// `workInProgress` tree is now current tree (during `componentDidMount`/`Update`).
root.current = finishedWork
// `Layout` phase:
// `useLayoutEffect` is called.
commitLayoutEffects(finishedWork, root, lanes)
// Tell Scheduler to yield at end of frame,
// so browser has an opportunity to paint.
requestPaint()
// Restore context.
executionContext = prevExecutionContext
setCurrentUpdatePriority(previousPriority)
ReactCurrentBatchConfig.transition = prevTransition
} else {
// No effects.
root.current = finishedWork
}
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects
if (rootDoesHavePassiveEffects) {
// This commit has passive effects:
// Stash a reference to them.
rootDoesHavePassiveEffects = false
rootWithPendingPassiveEffects = root
pendingPassiveEffectsLanes = lanes
} else {
// There were no passive effects:
// immediately release cache pool for this render.
releaseRootPooledCache(root, remainingLanes)
}
// Always call this before exiting `commitRoot`,
// to ensure that any additional work on this root is scheduled.
ensureRootIsScheduled(root, now())
// If passive effects are result of a discrete render,
// flush them synchronously at end of current task
// so that result is immediately observable.
if (
includesSomeLane(pendingPassiveEffectsLanes, SyncLane)
&& root.tag !== LegacyRoot
) {
flushPassiveEffects()
}
// If layout work was scheduled, flush it now.
flushSyncCallbacks()
return null
}
const BeforeMutationMask = Update | Snapshot | ChildDeletion | Visibility
const MutationMask
= Placement
| Update
| ChildDeletion
| ContentReset
| Ref
| Hydrating
| Visibility
const LayoutMask = Update | Callback | Ref | Visibility
Before Mutation Phase
Passiveeffects:FunctionComponentfiber (hooks): If there are pending passive effects, schedule a callback (asynchronous) to process them, as early as possible before anything else in commit phase.useXXXhooks normally run in asynchronous mode.useEffect(asynchronous) run afteruseLayoutEffect.
Snapshoteffects:HostRootfiber:HostConfig.clearContainer.ClassComponentfiber:instance.getSnapshotBeforeUpdate.
Deletioneffects:commitBeforeMutationEffectsDeletion->HostConfig.beforeActiveInstanceBlur.
// `Passive` effects.
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects()
return null
})
function flushPassiveEffects(): boolean {
// Returns whether passive effects were flushed.
if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
const priorityLevel
= pendingPassiveEffectsRenderPriority > NormalSchedulerPriority
? NormalSchedulerPriority
: pendingPassiveEffectsRenderPriority
pendingPassiveEffectsRenderPriority = NoSchedulerPriority
return runWithPriority(priorityLevel, flushPassiveEffectsImpl)
}
return false
}
function flushPassiveEffectsImpl() {
if (rootWithPendingPassiveEffects === null)
return false
rootWithPendingPassiveEffects = null
pendingPassiveEffectsLanes = NoLanes
// 1. 执行 effect.destroy().
const unmountEffects = pendingPassiveHookEffectsUnmount
pendingPassiveHookEffectsUnmount = []
for (let i = 0; i < unmountEffects.length; i += 2) {
const effect = unmountEffects[i]
const fiber = unmountEffects[i + 1]
const destroy = effect.destroy
effect.destroy = undefined
if (typeof destroy === 'function')
destroy()
}
// 2. 执行新 effect.create(), 重新赋值到 effect.destroy.
const mountEffects = pendingPassiveHookEffectsMount
pendingPassiveHookEffectsMount = []
for (let i = 0; i < mountEffects.length; i += 2) {
const effect = mountEffects[i]
const fiber = mountEffects[i + 1]
effect.destroy = create()
}
}
// `Snapshot` effects.
function commitBeforeMutationEffects(root: FiberRoot, firstChild: Fiber) {
HostConfig.prepareForCommit(root.containerInfo)
nextEffect = firstChild
// DFS traverse.
while (nextEffect !== null) {
const fiber = nextEffect
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
) {
// 1. Visit children.
nextEffect = child
} else {
while (nextEffect !== null) {
const fiber = nextEffect
commitBeforeMutationEffectsOnFiber(fiber)
const sibling = fiber.sibling
// 2. Visit sibling.
if (sibling !== null) {
nextEffect = sibling
break
}
nextEffect = fiber.return
}
}
}
}
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate
const flags = finishedWork.flags
if ((flags & Snapshot) !== NoFlags) {
switch (finishedWork.tag) {
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.
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
HostConfig.clearContainer(root.containerInfo)
}
break
}
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
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.'
)
}
}
}
}
function commitBeforeMutationEffectsDeletion(deletion: Fiber) {
if (doesFiberContain(deletion, focusedInstanceHandle)) {
shouldFireAfterActiveInstanceBlur = true
beforeActiveInstanceBlur(deletion)
}
}
Mutation Phase
ContentReseteffects:commitResetTextContent->HostConfig.resetTextContext.Refeffects:commitAttachRef/commitDetachRef->HostConfig.getPublicInstance.Visibilityeffects:SuspenseComponentfiber:markCommitTimeOfFallback.OffscreenComponentfiber:hideOrUnhideAllChildren->HostConfig.hideInstance/hideTextInstance/unhideInstance/unhideTextInstance.
Deletioneffects:commitDeletion->HostConfig.removeChild/removeChildFromContainer/clearSuspenseBoundaryFromContainer.Placementeffects:commitPlacement->insertOrAppendPlacementNode/insertOrAppendPlacementNodeIntoContainer->HostConfig.appendChild/insertBefore/appendChildToContainer/insertInContainerBefore.Updateeffects:commitWork->HostConfig.commitUpdate/commitTextUpdate/commitHydratedContainer/replaceContainerChildren.Hydratingeffects.
Effect Order
Deletion -> Insertion -> Update.
export function commitMutationEffects(
root: FiberRoot,
firstChild: Fiber,
committedLanes: Lanes
) {
inProgressLanes = committedLanes
inProgressRoot = root
nextEffect = firstChild
while (nextEffect !== null) {
const fiber = nextEffect
const deletions = fiber.deletions
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i]
commitDeletion(root, childToDelete, fiber)
}
}
const child = fiber.child
if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) {
// 1. Visit children.
nextEffect = child
} else {
while (nextEffect !== null) {
const fiber = nextEffect
commitMutationEffectsOnFiber(fiber, root, lanes)
const sibling = fiber.sibling
// 2. Visit sibling.
if (sibling !== null) {
nextEffect = sibling
break
}
nextEffect = fiber.return
}
}
}
inProgressLanes = null
inProgressRoot = null
}
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes
) {
const flags = finishedWork.flags
if (flags & ContentReset)
commitResetTextContent(finishedWork)
if (flags & Ref) {
const current = finishedWork.alternate
if (current !== null) {
// 先清空 ref, 在第三阶段 (Layout), 再重新赋值.
commitDetachRef(current)
}
if (finishedWork.tag === ScopeComponent)
commitAttachRef(finishedWork)
}
if (flags & Visibility) {
switch (finishedWork.tag) {
case SuspenseComponent: {
const newState: OffscreenState | null = finishedWork.memoizedState
const isHidden = newState !== null
if (isHidden) {
const current = finishedWork.alternate
const wasHidden = current !== null && current.memoizedState !== null
if (!wasHidden)
markCommitTimeOfFallback()
}
break
}
case OffscreenComponent: {
const newState: OffscreenState | null = finishedWork.memoizedState
const isHidden = newState !== null
const current = finishedWork.alternate
const wasHidden = current !== null && current.memoizedState !== null
const offscreenBoundary: Fiber = finishedWork
if (supportsMutation)
hideOrUnhideAllChildren(offscreenBoundary, isHidden)
break
}
}
}
const primaryFlags = flags & (Placement | Update | Hydrating)
switch (primaryFlags) {
case Placement: {
// Placement
commitPlacement(finishedWork)
finishedWork.flags &= ~Placement // Clear bit.
break
}
case PlacementAndUpdate: {
// Placement
commitPlacement(finishedWork)
finishedWork.flags &= ~Placement // Clear bit.
// Update
const current = finishedWork.alternate
commitWork(current, finishedWork)
break
}
case Hydrating: {
finishedWork.flags &= ~Hydrating // Clear bit.
break
}
case HydratingAndUpdate: {
finishedWork.flags &= ~Hydrating // Clear bit.
// Update
const current = finishedWork.alternate
commitWork(current, finishedWork)
break
}
case Update: {
const current = finishedWork.alternate
commitWork(current, finishedWork)
break
}
}
}
Layout Phase
Update | Callbackeffects:instance.componentDidMount/componentDidUpdate(synchronous).instancecallback forsetState.useLayoutEffect(synchronous).HostConfig.getPublicInstance/commitMount.
function commitLayoutEffects(
finishedWork: Fiber,
root: FiberRoot,
committedLanes: Lanes
): void {
inProgressLanes = committedLanes
inProgressRoot = root
nextEffect = finishedWork
while (nextEffect !== null) {
const fiber = nextEffect
const firstChild = fiber.child
if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
// 1. Visit children.
nextEffect = firstChild
} else {
while (nextEffect !== null) {
const fiber = nextEffect
if ((fiber.flags & LayoutMask) !== NoFlags) {
const current = fiber.alternate
commitLayoutEffectOnFiber(root, current, fiber, committedLanes)
}
// Complete `commitLayoutEffects`.
if (fiber === subtreeRoot) {
nextEffect = null
break
}
const sibling = fiber.sibling
// 2. Visit sibling.
if (sibling !== null) {
nextEffect = sibling
break
}
nextEffect = fiber.return
}
}
}
inProgressLanes = null
inProgressRoot = null
}
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
) {
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork)
}
break
}
case ClassComponent: {
const instance = finishedWork.stateNode
if (finishedWork.flags & Update) {
if (!offscreenSubtreeWasHidden) {
if (current === null) {
instance.componentDidMount()
} else {
const prevProps
= finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(
finishedWork.type,
current.memoizedProps
)
const prevState = current.memoizedState
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate
)
}
}
}
const updateQueue = finishedWork.updateQueue
if (updateQueue !== null) {
// 处理 update 回调函数, e.g: `this.setState({}, callback)`.
commitUpdateQueue(finishedWork, updateQueue, instance)
}
break
}
case HostRoot: {
const updateQueue = finishedWork.updateQueue
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
}
}
// 处理 update 回调函数, e.g: `this.setState({}, callback)`.
commitUpdateQueue(finishedWork, updateQueue, instance)
}
break
}
case HostComponent: {
const instance: Instance = finishedWork.stateNode
if (current === null && finishedWork.flags & Update) {
const type = finishedWork.type
const props = finishedWork.memoizedProps
commitMount(instance, type, props, finishedWork)
}
break
}
case SuspenseComponent: {
commitSuspenseHydrationCallbacks(finishedRoot, finishedWork)
break
}
case HostText:
case HostPortal:
case Profiler:
case SuspenseListComponent:
case IncompleteClassComponent:
case ScopeComponent:
case OffscreenComponent:
case LegacyHiddenComponent: {
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.'
)
}
}
// 重新设置ref.
if (finishedWork.flags & Ref)
commitAttachRef(finishedWork)
}