React Source Code

    5. React Fiber architecture: Creation, update, and submission of React Fiber objects

    Published
    November 20, 2022
    Reading Time
    6 min read
    Author
    Felix
    Access
    Public

    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:

    1. 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.
    2. 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:

    1. 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
      }
    }
    1. 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;
      }
    }
    1. 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:

    1. Fiber node reuse:

      • React uses the alternate attribute to implement reuse of Fiber nodes. Each Fiber node has an alternate that points to its corresponding node in another tree.
      • If alternate exists, React will reuse the node instead of creating a new one. This greatly reduces memory allocation and garbage collection overhead.
    2. 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.
    3. 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.

    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:

    1. Different types of elements will produce different tree structures.
    2. Developers can use the key attribute to indicate which sub-elements may be stable across different renderings.
    3. 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:

    1. Compare key and type
    2. Reuse or create new nodes
    3. 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:

    1. First traversal: try to update existing nodes
    2. Process new nodes
    3. Process nodes that need to be moved
    4. Delete old nodes that are no longer needed

    3.3.4 Optimization strategy of Diff algorithm

    1. 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.
    2. Use key for optimization: Key can be used to quickly determine whether an element can be reused.
    3. Compare from both ends to the middle: This strategy can quickly handle additions and deletions at the beginning and end.
    4. Use Map to store remaining nodes: Improve search efficiency.
    5. In-place reuse: Reuse existing Fiber nodes as much as possible.
    6. 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:

    1. Use the key attribute rationally, especially in list rendering, and avoid using indexes as keys.
    2. Try to maintain the stability of components and avoid unnecessary re-rendering.
    3. 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:

    1. Use the key attribute appropriately to help React better identify changes in elements in the list.
    2. Use React.memo, useMemo and useCallback to avoid unnecessary re-rendering.
    3. Understand and use life cycle methods and Hooks correctly to avoid expensive operations at inappropriate times.
    4. 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.

    Comments

    Join the conversation

    0 comments
    Sign in to comment

    No comments yet. Be the first to add one.