In the previous article, we discussed the overall structure and linked list of Fiber in detail. This article will delve into the creation process of Fiber objects, initial submission, and Fiber tree update mechanism.
1. Create Fiber object
The creation of a Fiber object is the starting point of the React rendering process. When we call ReactDOM.createRoot().render(), React starts creating the Fiber tree.
1.1 Create RootFiber
First, React will create a RootFiber, which is the root node of the entire Fiber tree.
function createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks) {
const root = new FiberRootNode(containerInfo, tag, hydrate)
const uninitializedFiber = createHostRootFiber(tag)
root.current = uninitializedFiber
uninitializedFiber.stateNode = root
initializeUpdateQueue(uninitializedFiber)
return root
}
function createHostRootFiber(tag) {
let mode
if (tag === ConcurrentRoot) {
mode = ConcurrentMode | BlockingMode | StrictMode
} else if (tag === BlockingRoot) {
mode = BlockingMode | StrictMode
} else {
mode=NoMode
}
return createFiber(HostRoot, null, null, mode)
}This process creates FiberRoot and RootFiber. FiberRoot is the starting point of the entire application, and RootFiber is the root node of the component tree.
1.2 Create sub-Fiber nodes
Next, React will recursively create child Fiber nodes based on the component tree. This process happens in the reconcileChildren function:
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
if (current === null) {
// If it is the first rendering, use mountChildFibers
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes)
} else {
// If it is an update, use reconcileChildFibers
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes)
}
}mountChildFibers and reconcileChildFibers are both return values of the ChildReconciler function. The difference lies in whether the side effects are marked.
1.3 Create a single Fiber node
The core logic of creating a single Fiber node is in the createFiberFromElement function:
function createFiberFromElement(element, mode, lanes) {
let owner = null
const type = element.type
const key = element.key
const pendingProps = element.props
const fiber = createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, lanes)
return fiber
}
function createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, lanes) {
let fiberTag = IndeterminateComponent
// Determine fiberTag based on component type
if (typeof type === 'function') {
if (shouldConstruct(type)) {
fiberTag = ClassComponent
}
} else if (typeof type === 'string') {
fiberTag = HostComponent
}
const fiber = createFiber(fiberTag, pendingProps, key, mode)
fiber.elementType = type
fiber.type = type
fiber.lanes = lanes
return fiber
}This process creates corresponding Fiber nodes based on the type, key, and props of the React element.
2. Initial submission
After creating the Fiber tree, React needs to render these virtual Fiber nodes into the actual DOM. This process is called "commit".
2.1 Preparation before submission
Before entering the submission phase, React will do some preparation work:
function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel()
runWithPriority(ImmediatePriority, commitRootImpl.bind(null, root, renderPriorityLevel))
return null
}
function commitRootImpl(root, renderPriorityLevel) {
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
}
root.finishedWork = null
root.finishedLanes = NoLanes
if (finishedWork === root.current) {
throw new Error(
'Cannot commit the same tree as before. This error is likely caused by ' + 'a bug in React. Please file an issue.'
)
}
// Commit phase begins
root.callbackNode = null
root.callbackPriority = NoLane
// Update remainingLanes and pendingLanes
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes)
markRootFinished(root, remainingLanes)
if (root === workInProgressRoot) {
workInProgressRoot = null
workInProgress = null
workInProgressRootRenderLanes = NoLanes
}
// Get the list of side effects
let firstEffect
if (finishedWork.flags > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork
firstEffect = finishedWork.firstEffect
} else {
firstEffect = finishedWork
}
} else {
firstEffect = finishedWork.firstEffect
}
//Start submitting three sub-phases
// ...
}Here, React sets the priority of the submission process to the highest (ImmediatePriority) to ensure that the submission process will not be interrupted. At the same time, it also handles some preparation work, such as refreshing passive effects and getting the side effects list.
Using the highest priority to execute the submission process ensures that updates can be applied to the DOM as soon as possible. I understand that there are several reasons:
- User experience: The submission phase directly affects the UI changes visible to the user. By giving them the highest priority, React ensures that these changes are presented to users as quickly as possible, improving the responsiveness of the application.
- Consistency guarantee: The submission phase needs to be executed synchronously to ensure the consistency of the UI. If this process is interrupted, it may leave the UI in an inconsistent state.
2.2 Submission phase
The submission phase is divided into three sub-phases: Before mutation, Mutation and Layout. The simplified code is as follows
function commitRootImpl(root, renderPriorityLevel) {
// ...
//First stage: Before mutation
commitBeforeMutationEffects()
//Second stage: Mutation
commitMutationEffects(root, renderPriorityLevel)
//Switch current tree
root.current = finishedWork
//The third stage: Layout
commitLayoutEffects(root, lanes)
// ...
}Each stage has specific tasks:
- Before mutation stage:
- Preparation work before handling DOM operations
-Scheduling useEffect
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
// ...
}
const flags = nextEffect.flags
if ((flags & Snapshot) !== NoFlags) {
commitBeforeMutationEffectOnFiber(current, nextEffect)
}
if ((flags & Passive) !== NoFlags) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects()
return null
})
}
}
nextEffect = nextEffect.nextEffect
}
}- Mutation stage:
- Perform actual DOM manipulation
- Call life cycle methods
- Reset text nodes
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
while (nextEffect !== null) {
const flags = nextEffect.flags;
if (flags & ContentReset) {
commitResetTextContent(nextEffect);
}
if (flags & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
case Placement: {
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement;
break;
}
case PlacementAndUpdate: {
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement;
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}- Layout stage:
- Handle work after DOM operations
- Call useLayoutEffect hook
- Update ref
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
while (nextEffect !== null) {
const flags = nextEffect.flags;
if (flags & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
if (flags & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}3. Fiber tree update mechanism
React's update mechanism is the core of its performance optimization. When the component state changes, React will create a new Fiber tree (called the workInProgress tree), and then compare it with the current Fiber tree to find out the parts that need to be updated.
3.1 Scheduling updates
When we call setState or use hooks to update the state, React will schedule an update. This part of the source code is actually discussed a lot in Chapter 3:
function scheduleUpdateOnFiber(fiber: Fiber, lane: Lane, eventTime: number) {
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
return null;
}
// Mark the root node as scheduled
markRootUpdated(root, lane, eventTime);
if (root === workInProgressRoot) {
// If we are working on this root node, the priority may need to be adjusted
if (
workInProgressRootExitStatus === RootSuspendedWithDelay ||
(workInProgressRootExitStatus === RootSuspended &&
includesOnlyRetries(workInProgressRootRenderLanes) &&
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
) {
//Interrupt current rendering
prepareFreshStack(root, NoLanes);
} else {
// Continue the current rendering and merge new updates
workInProgressRootRenderLanes = mergeLanes(
workInProgressRootRenderLanes,
lane,
);
}
}
ensureRootIsScheduled(root, eventTime);
if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode
) {
// Synchronous update, execute immediately
performSyncWorkOnRoot(root);
}
return root;
}3.2 Build workInProgress tree
In the update process of React, the core of this process is the createWorkInProgress function, which is responsible for creating or reusing Fiber nodes to build a new tree structure. We mentioned it earlier, but let’s take a deeper look at the design:
function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
// If alternate does not exist, create a new Fiber
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// If alternate exists, reset workInProgress
workInProgress.pendingProps = pendingProps;
workInProgress.type = current.type;
workInProgress.flags = NoFlags;
workInProgress.nextEffect = null;
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
// ...reset of other properties
}
//Copy some unchanged properties
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
// handle dependencies
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
//Copy other properties
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
return workInProgress;
}The main purpose of this function is to create or reuse a Fiber node as the unit of work in the current update process:
Fiber node reuse:
- React uses the
alternateattribute to implement reuse of Fiber nodes. Each Fiber node has analternatethat points to its corresponding node in another tree. - If
alternateexists, React will reuse the node instead of creating a new one. This greatly reduces memory allocation and garbage collection overhead.
- React uses the
Selective Reset:
- When reusing a Fiber node, React only resets those properties that may have changed (such as
pendingProps,flags,effects, etc.). - Immutable attributes (such as
childLanes,lanes,child, etc.) are copied directly from the current Fiber to avoid unnecessary operations.
- When reusing a Fiber node, React only resets those properties that may have changed (such as
Shallow copy:
- For most properties, React uses shallow copies. This means that reference type properties (such as
updateQueue,memoizedState, etc.) only copy the reference, rather than deep cloning (react is almost always a shallow copy). - This strategy is efficient in most cases, but also requires developers to be careful when manipulating these properties to avoid accidental modifications.
- For most properties, React uses shallow copies. This means that reference type properties (such as
3.3 Diff algorithm
React's Diff algorithm is the core of its efficient update, which is used to compare the differences between two virtual DOM trees to minimize actual DOM operations. React’s Diff algorithm is based on three main assumptions:
- Different types of elements will produce different tree structures.
- Developers can use the key attribute to indicate which sub-elements may be stable across different renderings.
- Only compare elements of the same level.
3.3.1 Entrance to Diff algorithm
The entrance to the Diff algorithm is the reconcileChildFibers function, which is used in almost all update and rendering processes:
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes
): Fiber | null {
// Process a single child element
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes
)
);
// ...other types of processing
}
}
// handle multiple child elements
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes
);
}
// Process text nodes
if (typeof newChild === 'string' || typeof newChild === 'number') {
return placeSingleChild(
reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
lanes
)
);
}
// Otherwise, delete all existing child nodes
return deleteRemainingChildren(returnFiber, currentFirstChild);
}3.3.2 Single node comparison
For a single child element, React uses the reconcileSingleElement function for comparison:
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: lanes
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// compare keys
if (child.key === key) {
// compare types
if (child.elementType === element.type) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber;
return existing;
}
//The key is the same but the type is different, delete all old child nodes
deleteRemainingChildren(returnFiber, child);
break;
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
//Create a new Fiber node
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}The process of single-node comparison mainly includes:
- Compare key and type
- Reuse or create new nodes
- Delete old nodes that are not needed
3.3.3 Multi-node comparison
For multiple child elements, React uses the reconcileChildrenArray function for comparison:
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<any>,
lanes: Lanes
): Fiber | null {
let resultingFirstChild: Fiber | null = null;
let previousNewFiber: Fiber | null = null;
let oldFiber = currentFirstChild;
let lastPlacedIndex = 0;
let newIdx = 0;
let nextOldFiber = null;
// First traversal: process updated nodes
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
// All new child nodes have been processed
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// All old child nodes have been processed, add the remaining new nodes
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
//Add the remaining old nodes to the Map for subsequent searches
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// Second traversal: process the remaining new nodes and try to reuse old nodes from the Map
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// Delete the remaining old nodes in the Map
if (shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}The process of multi-node comparison mainly includes:
- First traversal: try to update existing nodes
- Process new nodes
- Process nodes that need to be moved
- Delete old nodes that are no longer needed
3.3.4 Optimization strategy of Diff algorithm
- Two traversals: The first time is to process nodes that can be directly updated, and the second time is to process nodes that need to be moved or newly created.
- Use key for optimization: Key can be used to quickly determine whether an element can be reused.
- Compare from both ends to the middle: This strategy can quickly handle additions and deletions at the beginning and end.
- Use Map to store remaining nodes: Improve search efficiency.
- In-place reuse: Reuse existing Fiber nodes as much as possible.
- Batch processing: Batch multiple update operations to reduce the number of renderings.
Through these optimization strategies, React's Diff algorithm can complete the comparison within O(n) time complexity, greatly improving performance. Understanding how the Diff algorithm works is important for optimizing React applications, for example:
- Use the
keyattribute rationally, especially in list rendering, and avoid using indexes as keys. - Try to maintain the stability of components and avoid unnecessary re-rendering.
- Split components reasonably to avoid large-scale Diff caused by large components.
3.4 Complete update
Once the diff is complete, React marks the Fiber nodes that need updating and then applies those updates during the commit phase. This process is similar to the commit phase during initial rendering, but only nodes marked as requiring updates will be processed.
Summary
The React Fiber architecture greatly improves the performance and responsiveness of React applications through interruptible rendering processes and fine update control. By creating, updating, and submitting Fiber objects, React can flexibly manage the component tree and achieve efficient state updates and DOM operations. Understanding these internal mechanisms is crucial to deeply mastering React and optimizing React applications.
In actual development, we can use this knowledge to optimize our React applications:
- Use the
keyattribute appropriately to help React better identify changes in elements in the list. - Use
React.memo,useMemoanduseCallbackto avoid unnecessary re-rendering. - Understand and use life cycle methods and Hooks correctly to avoid expensive operations at inappropriate times.
- For large lists, consider using virtualization technology (such as
react-window) to reduce the number of nodes rendered.
By deeply understanding how React Fiber works, we can write more efficient and maintainable React applications.