Summary of background

The React Diff phase is asynchronous and interruptible. Fiber split the React update process into two phases:

  1. Render phase: asynchronous interruptible diff old and new DOM, does not update immediately after finding differences. Instead, click on the Fiber nodetag(the Update/Placement/Delete).
  2. The Commit phase:Traverse the Fiber with the tag, according totagPerforms the corresponding DOM update of the.

This article focuses on the render phase of the update process. In React, the render process can be split into beginWork and completeWork. Here is the work of the two processes.

An overview of the process

Scheduling time

React creates an Update for each state Update and adds it to the task queue. The final Schedule will Schedule tasks based on the corresponding priority line of the task queue. The schematic process is as follows:

Every Update in React goes through the Render and Commit process. The Render phase mainly completes the creation/change of fiber nodes and tags them accordingly. Render will perform depth-first traversal starting from the rootFiber node. In the depth-first process, the BeginWork function is executed in the recursive stage, and the CompleteWork function is executed in the return stage.

export default function App() {
  return (
    <div className="App">
      <div>
        hello
        <span> world </span>
      </div>
    </div>
  );
}
Copy the code

The corresponding Fiber node generated by the App component above is as follows:

The scheduling order of the corresponding Fiber’s BeginWork/CompleteWork is as follows:

  1. BeginWork phase of the Root node
  2. The BeginWork phase of the App node
  3. The BeginWork phase of the DIV node
  4. The CompleteWork stage of the div node
  5. Span BeginWork phase of a node
  6. The CompleteWork stage of the SPAN node (optimised inside React, if there is only one Text node skip BeginWork/CompleteWork)
  7. The CompleteWork stage of the div node
  8. The CompleteWork stage of the App node
  9. The CompleteWork stage of the Root node

BeginWork

In the update process, React depth traverses from FiberRoot (Root). BeginWork is performed once for each Fiber node. Eventually a Child Fiber is returned. BeginWork is generally summarized as follows:

React Diff the workInProgress node and its current node and Flag them accordingly.

BeginWork main process

The BeginWork method is executed in the performUnitOfWork function. The specific logic is as follows:

// According to the dual-cache mechanism, current is the Fiber node of the current page rendering, which is null during mount. WorkInProgress is the Dom node that needs to be rendered on the browser this time
// renderLanes is the priority of this update, which will be discussed later. You just need to know here
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) :Fiber | null {
  // Get the priority of this Update
  const updateLanes = workInProgress.lanes;
  // Check whether current is mount or update
  if(current ! = =null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if(oldProps ! == newProps || hasLegacyContextChanged()) { didReceiveUpdate =true;
    } else if(! includesSomeLane(renderLanes, updateLanes)) {Fiber Update priority is inconsistent with this scheduling Update priority
      didReceiveUpdate = false; .returnbailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); }}else {
    didReceiveUpdate = false;
  }
  workInProgress.lanes = NoLanes;
  // Go to different branches according to Fiber type
  switch (workInProgress.tag) {
    // a function component that goes IndeterminateComponent(in the anonymous component logic) when first mounted,
    // After execution, the tag determines whether it is a Class component or a Function component based on the render Function
    case IndeterminateComponent: { // Component of unknown type
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderLanes,
      );
    }
    case FunctionComponent: { // Function components.return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case ClassComponent: { / / Class components.return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case HostRoot: / / FiberRoot node
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent: // The DOM node corresponds to Fiber
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText: / / Text node
      returnupdateHostText(current, workInProgress); }}Copy the code

Determine whether current is in the Mount or Update phase according to whether current exists

React’s dual-cache mechanism allows current Fiber to store the Fiber nodes for the current page rendering. If the object is empty. It indicates that the current page has no corresponding DOM node, and the Mount stage is displayed. Otherwise, the Update stage is displayed.

Check whether the node needs to be reused

React internally determines whether a node needs to be reused based on the didReceiveUpdate. If this variable is false. Then directly reuse the DOM node of the previous Fiber.

The mount process varies according to the tag

The beginWork will eventually go to different branch logic according to the different tag. The main logic of these functions is basically the same. The main logic is to create the corresponding Child Fiber. And for the current node/child nodes play the Placement/Deletion/Update mark in the Fiber needs to place/delete/Update.

Process of mounting a function component

Take the IndeterminateComponent type as an example. When a function component is first mounted, its tag is IndeterminateComponent. In mountIndeterminateComponent, performs the function component of logic, generate a Fiber and its corresponding DOM structure.

function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes,) {
    // If it is mount phase, empty the current alternate in Fiber.
    // Mark current Fiber with Placement.
    if(_current ! = =null) {
      _current.alternate = null;
      workInProgress.alternate = null;
      // Since this is conceptually a new fiber, schedule a Placement effect
      workInProgress.flags |= Placement;
    }
    const props = workInProgress.pendingProps;
    let value;
    // Execute the function component to get the corresponding DOM structure
    value = renderWithHooks(
        null,
        workInProgress,
        Component,
        props,
        context,
        renderLanes,
    )
    // React DevTools reads this flag.
    workInProgress.flags |= PerformedWork;
    // Set the tag to FunctionComponent
    workInProgress.tag = FunctionComponent;
    // Diff algorithm, Flag Placement/Update/Delete for Child Fiber
    reconcileChildren(null, workInProgress, value, renderLanes);
    // Return the next Fiber node for which the beginWork is required
    // Fiber has no child node for beginWork, so beginWork starts from this node
    return workInProgress.child;
  }  
Copy the code

The rest of updateFunctionComponent/ updateClassComponent has similar logic and will not be explained.

CompleteWork

When the BeginWork ends, the child node of the current Fiber node is returned. If the Child node is null, the current node can no longer traverse down, and the completeUnitOfWork logic for the current node is executed. The completeWork function does the following:

  • fortypeHostRoot , HostComponent , HostTextAnd so on.DOMFiberNode. itDOMInserts from the children into the parent node in sequenceDOM, and assign the valuestateNodeProperties.
  • There will bePlacement / Deletion / UpdatetagString into a linked list, convenientcommitStage ofFiberProcess.
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;
  let next;
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    unitOfWork.memoizedProps = unitOfWork.pendingProps;
    if (next === null) {
    // If it is a leaf of the Fiber tree, completeWork is executed
      completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
  ReactCurrentOwner.current = null;
}
Copy the code

DOM structure creation and insertion

The completeWork inserts the DOM of the byte points in turn into the DOM of the parent node and stores them in the stateNode property. Render it to the browser during the COMMIT phase. The code is as follows:

    case HostComponent: {
      popHostContext(workInProgress);
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;
      if(current ! = =null&& workInProgress.stateNode ! =null) {
        // Update the Fiber
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );
        if (current.ref !== workInProgress.ref) {
          markRef(workInProgress);
        }
      } else {
          // Create the DOM node corresponding to the current Fiber
          const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
          // Add the DOM node corresponding to the child node to the current Fiber
          appendAllChildren(instance, workInProgress, false.false);
          workInProgress.stateNode = instance;
         if (
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            markUpdate(workInProgress);
          }
        if(workInProgress.ref ! = =null) {
          // If there is a ref on a host node we need to schedule a callbackmarkRef(workInProgress); }}return null;
    }
  // Mount the workInPrgress to the DOM node corresponding to the parent node
  const appendAllChildren = function(
    parent: Instance,
    workInProgress: Fiber,
    needsVisibilityToggle: boolean,
    isHidden: boolean.) {
    // We only have the top Fiber that was created but we need recurse down its
    // children to find all the terminal nodes.
    let node = workInProgress.child;
    while(node ! = =null) {
      // If Fiber is a HostComponent or HostText node, insert it into the parent node
      if (node.tag === HostComponent || node.tag === HostText) {
        appendInitialChild(parent, node.stateNode);
      } else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
        // If it is a function component
        appendInitialChild(parent, node.stateNode.instance);
      } else if (node.tag === HostPortal) {
        // If we have a portal child, then we don't want to traverse
        // down its children. Instead, we'll get insertions from each child in
        // the portal directly.
      } else if(node.child ! = =null) {
        / / recursion
        node.child.return = node;
        node = node.child;
        continue;
      }
      if (node === workInProgress) {
        return;
      }
      // Process sibling nodes
      while (node.sibling === null) {
        if (node.return === null || node.return === workInProgress) {
          return; } node = node.return; } node.sibling.return = node.return; node = node.sibling; }};Copy the code

For example, the DOM mounted in the Fiber corresponding to the App component is as follows.

export default function App() {
  return (
    <div className="App">
      <div>
        hello
        <span> world </span>
      </div>
    </div>
  );
}
Copy the code

The establishment of the EffectList

In the beginWork, React marks the corresponding flag for all fibers that need to be changed. These flags are consumed during the COMMIT phase. In order to better consume the flag in Fiber. React sets its Child Fiber flag to the subtreeFlags of the current Fiber. SubtreeFlags: If the current Fiber child has a flag, subtreeFlags will have a value.

function bubbleProperties(completedWork: Fiber) {
  let subtreeFlags = NoFlags;
  let child = completedWork.child;
  while(child ! = =null) {
    newChildLanes = mergeLanes(
      newChildLanes,
      mergeLanes(child.lanes, child.childLanes),
    );
    subtreeFlags |= child.subtreeFlags & StaticMask;
    subtreeFlags |= child.flags & StaticMask;
    child.return = completedWork;
    child = child.sibling;
  }
  completedWork.subtreeFlags |= subtreeFlags;
}
Copy the code

After this processing, during the COMMIT phase, the subtreeFlags attribute on Fiber tells you if there is a Child Fiber flag under the current Fiber. Improved efficiency in the COMMIT phase.

Render Renders the Error component

Error component processing flow

As mentioned above, the React component executes in the beginWork phase.

As you can see from the source code below, in the dev phase, when the originalBeginWork execution fails, it will go to the catch to reset the attribute and re-execute the beginWork function.

  beginWork = (current, unitOfWork, lanes) = > {
    // Copy the current Fiber if beginWork fails. Then use the copied Fiber to reset
    const originalWorkInProgressCopy = assignFiberPropertiesInDEV(
      dummyFiber,
      unitOfWork,
    );
    try {
      return originalBeginWork(current, unitOfWork, lanes);
    } catch (originalError) {
      // Keep this code in sync with handleError; any changes here must have
      // corresponding changes there
      resetContextDependencies();
      resetHooksAfterThrow();
      // Don't reset current debug fiber, since we're about to work on the
      // same fiber again.
      // Unwind the failed stack frame
      unwindInterruptedWork(unitOfWork, workInProgressRootRenderLanes);
      // Restore the original properties of the fiber.
      assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy);
      // Run beginWork again.
      invokeGuardedCallback(
        null,
        originalBeginWork,
        null,
        current,
        unitOfWork,
        lanes,
      );
      // We always throw the original error in case the second render pass is not idempotent.
      // This can happen if a memoized function or CommonJS module doesn't throw after first invocation.
      throworiginalError; }};Copy the code

If the beginWork phase of Fiber fails again, the outer try… Catch is caught and handled by the handleError function. HandleError will eventually go to

try {
  workLoopConcurrent();
  break;
} catch (thrownValue) {
  handleError(root, thrownValue);
}

 
function handleError(root, thrownValue) :void {
  do {
    let erroredWork = workInProgress;
    try {
      // Reset module-level state that was set during the render phase.
      resetContextDependencies();
      resetHooksAfterThrow();
      resetCurrentDebugFiberInDEV();
      // TODO: I found and added this missing line while investigating a
      // separate issue. Write a regression test using string refs.
      ReactCurrentOwner.current = null;
      // Special treatment for root Fiber as ErrorBoundary can only handle subcomponents
      if (erroredWork === null || erroredWork.return === null) {
        // Expected to be working on a non-root fiber. This is a fatal error
        // because there's no ancestor that can handle it; the root is
        // supposed to capture all errors that weren't caught by an error
        // boundary.
        workInProgressRootExitStatus = RootFatalErrored;
        workInProgressRootFatalError = thrownValue;
        workInProgress = null;
        return;
      }
      // End up in throwException
      throwException(
        root,
        erroredWork.return,
        erroredWork,
        thrownValue,
        workInProgressRootRenderLanes,
      );
      completeUnitOfWork(erroredWork);
    } catch (yetAnotherThrownValue) {
       // If other exceptions are thrown, the loop continues. Until the process is complete
      thrownValue = yetAnotherThrownValue;
      if(workInProgress === erroredWork && erroredWork ! = =null) {
        // If this boundary has already errored, then we had trouble processing
        // the error. Bubble it to the next boundary.
        erroredWork = erroredWork.return;
        workInProgress = erroredWork;
      } else {
        erroredWork = workInProgress;
      }
      continue;
    }
    // Return to the normal work loop.
    return;
  } while (true);
} 
Copy the code
  1. throwExceptionCan go wrong for thisFiberIn the playImcompleteeffect flags. Indicates that theFiberrenderPhase not completed.
  2. willworkInProgressRootExitStatusSet toRootErroredIn allFiber
  3. It also looks up the most recent existencegetDerivedStateFromErrorcomponentDidCatchClass Component(hereinafter referred toErrorBoundaryComponent) as the error boundary. Type it when you find itShouldCaptureflagIndicates that the component requires error handling.
  4. Based on theErrorBoundaryComponent creates aupdate.getDerivedStateFromErrorupdatepayload , componentDidCatchcallbackAnd finally made the teamErrorBoundaryThe component’supdateQueueIn the.
  5. Out of the nodebeginWorkStage, entercompleteWorkPhase.
function throwException(root: FiberRoot, returnFiber: Fiber, sourceFiber: Fiber, value: mixed, rootRenderLanes: Lanes,) {
  // The source fiber did not complete.
  sourceFiber.flags |= Incomplete;
  if( value ! = =null &&
    typeof value === 'object' &&
    typeof value.then === 'function'
  ) {
    const wakeable: Wakeable = (value: any);
    resetSuspendedComponent(sourceFiber, rootRenderLanes);
    renderDidError();
    value = createCapturedValue(value, sourceFiber);
    let workInProgress = returnFiber;
    do {
      switch (workInProgress.tag) {
        case HostRoot: {
          const errorInfo = value;
          workInProgress.flags |= ShouldCapture;
          const lane = pickArbitraryLane(rootRenderLanes);
          workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
          const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
          enqueueCapturedUpdate(workInProgress, update);
        return;
      }

      case ClassComponent:
        // Capture and retry
        const errorInfo = value;
        const ctor = workInProgress.type;
        const instance = workInProgress.stateNode;
        if (
          (workInProgress.flags & DidCapture) === NoFlags &&
          (typeof ctor.getDerivedStateFromError === 'function'|| (instance ! = =null &&
              typeof instance.componentDidCatch === 'function' &&
              !isAlreadyFailedLegacyErrorBoundary(instance)))
        ) {
          workInProgress.flags |= ShouldCapture;
          const lane = pickArbitraryLane(rootRenderLanes);
          workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
          // Schedule the error boundary to re-render using updated state
          const update = createClassErrorUpdate(
            workInProgress,
            errorInfo,
            lane,
          );
          enqueueCapturedUpdate(workInProgress, update);
          return;
        }
        break;
      default:
        break;
    }
    workInProgress = workInProgress.return;
  } while(workInProgress ! = =null);
}
Copy the code

Finally, it is the completeWork stage of Fiber. What needs to be noticed is that the flag of Fiber is Incomplete at this time. The nearest ErrorBoundary component is found up here and the new beginWork logic is reexecuted from this node to generate the Fallback component.

function completeUnitOfWork(unitOfWork: Fiber) :void {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    // Check if the work completed or if something threw.
    if ((completedWork.flags & Incomplete) === NoFlags) {
    } else {
      // If completedWork is a component that can handle errors, next is that component, otherwise null
      const next = unwindWork(completedWork, subtreeRenderLanes);
      // Since this component can handle Error, set it to workInProgress Fiber
      if(next ! = =null) {
        next.flags &= HostEffectMask;
        workInProgress = next;
        return;
      }
      // Set its parent to Incomplete until a component that can handle errors is found.
      if(returnFiber ! = =null) {
        // Mark the parent fiber as incomplete and clear its subtree flags.
        returnFiber.flags |= Incomplete;
        returnFiber.subtreeFlags = NoFlags;
        returnFiber.deletions = null; }}// If there are sibling nodes, set the sibling to workInProgress Fiber
    // Otherwise set the parent node as the component of the next workInProgress
    const siblingFiber = completedWork.sibling;
    if(siblingFiber ! = =null) {
      // If there is more work to do in this returnFiber, do that next.
      workInProgress = siblingFiber;
      return;
    }
    // Otherwise, return to the parent
    completedWork = returnFiber;
    // Update the next thing we're working on in case something throws.
    workInProgress = completedWork;
  } while(completedWork ! = =null);

  // We've reached the root.
  if(workInProgressRootExitStatus === RootIncomplete) { workInProgressRootExitStatus = RootCompleted; }}Copy the code

The process to comb

Using the following Fiber structure as an example, the ErrorBoundary component can handle errors where the App component throws an Error. The entire rendering process is as follows

  1. fromRoot FiberStart down depth prioritybeiginWorkThe processing of
  2. Handle to theApp FiberThrow aError
  3. Back in theApp FiberforbeginWorkProcess, throw againError
  4. forApp FiberIn the playIncompleteflagFind the nearest one you can handleErrorThe components of theErrorBoudary,shouldCaptrueflag. And for theFiberLet’s add onepayloadgetDerivedStateFromErrorupdate.
  5. AppComponents to performcompleteWork, up to findErrorBoundaryComponents,ErrorBoundaryComponent handlesupdateQueueTo obtain a newstate. According to thestateUpdate to thefallback UI.
  6. Back tofallback UIperformbeginWork, according to the normal processrenderStage of processing
  7. forcommitStage, browser renderingfallback UIAnd the correspondingDOM

The articles

1.React Design Idea

Refer to the article

  1. Juejin. Cn/post / 691962…
  2. Juejin. Cn/post / 691779…
  3. react.iamkasong.com/