Author: Liu Peng

preface

As we know, React Fiber is the new Reconciliation engine in React V16, a two-year rewrite of the core algorithm for the Stack Reconciler version by the React team. Its main goal is to achieve incremental rendering of the virtual DOM, the ability to split the rendering effort into chunks and spread it out over multiple frames. The ability to pause, abort, or reuse work as new updates arrive, and the ability to prioritize different types of updates. This is a reflection of React’s design. In contrast to the “push” approach used by some popular libraries in the industry, React sticks to the “pull” approach, delaying calculations until necessary. That sounds interesting.

React 16.8.6: Parallel rendering. React 16.8.6: Parallel rendering. React 16.8.6: Parallel rendering.

Concept of premise

Before we dive into the React Fiber architecture, it’s worth tossing out a few core concepts that are useful for understanding how the React Fiber architecture works. It doesn’t matter if you don’t understand these basic concepts at first, but if you gradually understand and dig into the whole Fiber mechanism, you will gradually understand how react proposed these concepts.

Fiber object

File location: packages/react – the reconciler/SRC/ReactFiber. Js

A Fiber object is a basic unit that represents work.

Each React element corresponds to a Fiber object, and fibers is a linked list based on the Child, Sibling, and return properties. The core properties and meanings of the Fiber object are as follows:

Fiber = {// Identify The label of Fiber type. For details, see The following WorkTag: WorkTag, // The local state associated with this Fiber. See stateNode stateNode: any, // The Fiber to return to after finishing processing this one. // This is effectively the parent, But there can be multiple parents (two) // so this is only the parent of the thing we're currently processing Return: Fiber | null, / / Singly Linked List Tree Structure. / / the child to child nodes: Fiber | null, / / to the node (: Fiber | null, // Input is the data coming into process this fiber. Arguments. Props. // This type will be more specific once we // Set the props parameter when starting execution, pendingProps: Any, // The props used to create The output. // At The end of The memoizedProps: any, // A queue of state updates and callbacks. updateQueue: UpdateQueue < any > | null, / / The state, informs The to create The output / / current state memoizedState: Any, // effectTag effectTag: // Singly Linked List Fast path to the next fiber with side-effects. nextEffect: Fiber | null, // The first and last fiber with side-effect within this subtree. This allows // us to reuse a slice of the linked list when we reuse the work done within // this fiber. firstEffect: Fiber | null, lastEffect: Fiber | null, // Represents a time in the future by which this work should be completed. expirationTime: ExpirationTime, // This is used to quickly determine if a subtree has no pending changes. childExpirationTime: ExpirationTime, // This is a pooled version of a Fiber. Every fiber that gets updated will // eventually have a pair. There are cases when we can clean up pairs to save // memory if we need to. alternate: Fiber | null, }Copy the code

Create a Fiber object from the React element

File location: the react – the reconciler/SRC/ReactFiber. Js

export function createFiberFromElement(
  element: ReactElement,
  mode: TypeOfMode,
  expirationTime: ExpirationTime,
): Fiber {
  const fiber = createFiberFromTypeAndProps(
    type,
    key,
    pendingProps,
    owner,
    mode,
    expirationTime,
  );
  return fiber;
}
Copy the code

Child, Silbing, return

Properties of the Fiber object, which point to other Fibers and represent the next unit of work for the current unit of work, used to describe the recursion tree structure of fiber.

Child: the child of the parent fiber node. Silbing: the sibling of the parent fiber node. Return: the parent of the parent fiber node

Compared to React V16, the single-linked list structure of the Child, SIBing, and Return properties of the Fiber object, as well as the context information stored in the Fiber object, allows Scheduler to pause, pause, and restart concurrent modes.

work

In the React Reconciliation process, various activities such as the state update, props update, and refs update that must perform computation are collectively called “work” in Fiber architecture.

workTag

File location: shared/ reactworktags.js

The workTag type is used to describe the type of a React element, i.e. Fiber.

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedSuspenseComponent = 18;
export const EventComponent = 19;
export const EventTarget = 20;
export const SuspenseListComponent = 21;
Copy the code

stateNode

A reference to a component, a DOM node, or other instance of the React element associated with the Fiber node. In general, we can say that this property is used to hold the local state associated with a fiber. This is the fiber.statenode of the fiber object.

effectTag

File location: Shared/ReactSideEffectTags. Js

We usually refer to manual DOM changes or data requests, subscriptions and other operations performed during the life cycle that cannot be done in the Render phase as side Effects. In addition to common effects such as update, fiber nodes provide a convenient mechanism to track effects. Each fiber node has an effCT associated with it, which is encoded as the corresponding value of the effectTag field, fiber.effectTag, as shown below:

// Don't change these two values. They're used by React Dev Tools.
export const NoEffect = /*              */ 0b000000000000;
export const PerformedWork = /*         */ 0b000000000001;

// You can change the rest (and add more).
export const Placement = /*             */ 0b000000000010;
export const Update = /*                */ 0b000000000100;
export const PlacementAndUpdate = /*    */ 0b000000000110;
export const Deletion = /*              */ 0b000000001000;
export const ContentReset = /*          */ 0b000000010000;
export const Callback = /*              */ 0b000000100000;
export const DidCapture = /*            */ 0b000001000000;
export const Ref = /*                   */ 0b000010000000;
export const Snapshot = /*              */ 0b000100000000;
export const Passive = /*               */ 0b001000000000;

// Passive & Update & Callback & Ref & Snapshot
export const LifecycleEffectMask = /*   */ 0b001110100100;

// Union of all host effects
export const HostEffectMask = /*        */ 0b001111111111;

export const Incomplete = /*            */ 0b010000000000;
export const ShouldCapture = /*         */ 0b100000000000;
Copy the code

priority

File location: the react – the reconciler/SRC/SchedulerWithReactIntegration. Js

In Fiber architecture, some numbers represent the priority of work. The following lists the different priority levels and what they represent:

// We use ascending numbers so we can compare them like numbers. 
export const ImmediatePriority: ReactPriorityLevel = 99;
export const UserBlockingPriority: ReactPriorityLevel = 98;
export const NormalPriority: ReactPriorityLevel = 97;
export const LowPriority: ReactPriorityLevel = 96;
export const IdlePriority: ReactPriorityLevel = 95;
Copy the code

Reconciliation and the Scheduling

Reconciliation: React Compares Vdom with diff algorithm, which is used to confirm which parts need to be changed. Scheduling_ : _ Is used to determine when work is executed.

Render phase and Commit phase

This is a diagram of the Life cycle stages drawn by Dan Abramov of the React team. He divided the React life cycle into two phases: Render and Commit. The Commit stage can be further divided into pre-commit stage and Commit stage, as shown in the following figure:


During the Render phase, it is important to understand that work can be executed asynchronously. React can process one or more fiber nodes depending on the available time, then stop and save the finished work, freeing up time to process some events. It then picks up where it left off. Sometimes, however, it is possible to discard the work already done and start over from the top. These pauses are possible because the work performed at this stage does not result in any user-visible changes (such as DOM updates). In contrast, during the COMMIT phase, work execution is always synchronous. This is because the work performed in this phase will result in changes visible to the user, such as DOM updates. This is why React requires a one-time commit to complete the work.

The following life cycles are considered unsafe during the Render phase starting from version 16.3 and will be removed in future releases. The above life cycle legend is not included as listed below:

  • [UNSAFE_]componentWillMount (deprecated)
  • [UNSAFE_]componentWillReceiveProps (deprecated)
  • [UNSAFE_]componentWillUpdate (deprecated)

These unsafe lifecycle markers are often misunderstood and even abused, and some developers tend to put code with side effects in these methods, which can cause problems under the new asynchronous render methods. They are still likely to cause problems in the upcoming Concurrent mode.

Current tree and workInProgress tree

After the first rendering, React generates a Fiber tree for rendering the UI and mapping the application state, commonly referred to as the Current tree. When React traverses the Current tree, it creates an alternate node for each existing Fiber node with the alternate property, which forms the workInProgress tree.

All work updates occur in the workInProgress tree. If the alternate property has not been created, React will create a copy of the current tree in createWorkInProgress before processing the update. The workInProgress tree is formed to map the new state and refresh it to the screen during the COMMIT phase.

Effects list

The effect List is based on the fiber object’s linked list structure consisting of firstEffect, nextEffect, and lastEffect properties. As shown below:


When the Render phase is over, an Effect list is generated. During the COMMIT phase, React loops through the effect list and checks the fiber’s effect type. When it finds a function that is related to the fiber’s effect type, it applies the call, such as DOM changes and lifecycle function calls.

The core code logic for the effect List construction is as follows:

File location: the react – the reconciler/SRC/ReactFiberWorkLoop. Js

function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { // Append all the effects of the subtree and this fiber onto the effect // list of the parent. The completion order of the children affects the // side-effect order. if (returnFiber.firstEffect === null) { returnFiber.firstEffect = workInProgress.firstEffect; } if (workInProgress.lastEffect ! == null) { if (returnFiber.lastEffect ! == null) { returnFiber.lastEffect.nextEffect = workInProgress.firstEffect; } returnFiber.lastEffect = workInProgress.lastEffect; } if (returnFiber.lastEffect ! == null) { returnFiber.lastEffect.nextEffect = workInProgress; } else { returnFiber.firstEffect = workInProgress; } returnFiber.lastEffect = workInProgress; }Copy the code

The entire build process can be summarized as follows:

1) When returnFiber.firstEffect and ReturnFiber. lastEffect are both null, FirstEffect =workInProgress1 and lastEffect=workInProgress1.

2) at this time to continue to find, when find workInProgress2 workInProgress1 brother nodes, execute returnFiber. LastEffect. NextEffect = workInProgress2, The purpose is to workInProgress1 nextEffect = workInProgress2. ReturnFiber. LastEffect = workInProgress2, i.e. update returnFiber’s lastEffect to workInProgress2.

3) at this point, the formation of workInProgress1. NextEffect = workInProgress2, returnFiber. FirstEffect = workInProgress1, Returnfiber. lastEffect = workInProgress2, continue to find the same node and the parent node, and so on.

For more details, see the Section “The Process for Building an Effect-List Fiber Linked List” at the bottom of this article. This video shows the process in detail.

Render phase

In this article, we mainly focus on the implementation of the updater object in the ReactDOM, using the class component as an example, which is a classComponentUpdater used to get fiber instances, update queues, and schedule work. Let’s assume we’ve already started calling a setState method.

enqueueSetState

Each React component has an associated updater that acts as a bridge between the component and the React core library. This allows setState to be implemented in a variety of ways, including ReactDOM, React Native, server-side rendering and testing tools.

Mounted on the prototype object of Component function setState method, the Component instance objects. This updater. EnqueueSetState

File location: the react/SRC/ReactBaseClasses. Js

Function Component(props, context, updater) {this. Props = props; this.context = context; // If a component has string refs, we will assign a different object later. this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the // renderer. this.updater = updater || ReactNoopUpdateQueue; } / / Component prototype object mount setState Component. The prototype. The setState = function (partialState, callback) { this.updater.enqueueSetState(this, partialState, callback, 'setState') }Copy the code

The enqueueSetState method looks like this:

File location: the react – the reconciler/SRC/ReactFiberClassComponent. Js

EnqueueUpdate (Fiber, update) : Inserts updates into queue scheduleWork(Fiber, expirationTime) : schedules work

Const classComponentUpdater = {/* inst: payload: */ enqueueSetState(Inst, payload, Const fiber = getInstance(inst); const currentTime = requestCurrentTime(); / / calculate due time const expirationTime = computeExpirationForFiber (currentTime, fiber, suspenseConfig,); const update = createUpdate(expirationTime, suspenseConfig); update.payload = payload; enqueueUpdate(fiber, update); scheduleWork(fiber, expirationTime); }}Copy the code

enqueueUpdate

File location: the react – the reconciler/SRC/ReactUpdateQueue. Js

UpdateQueue is a linked list of prioritized updates.

Like Fiber, update queues come in pairs: a current queue representing on-screen visible state and a work-in-progress queue that can be asynchronously computed and processed prior to the COMMIT phase. If a work-in-progress queue is discarded before completion, a new work-in-progress queue is created by cloning a Curent queue.

The core code logic is as follows:

export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) { // Update queues are created lazily. const alternate = fiber.alternate; let queue1; let queue2; if (alternate === null) { // There's only one fiber. queue1 = fiber.updateQueue; queue2 = null; if (queue1 === null) { queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState); } } else { // There are two owners. queue1 = fiber.updateQueue; queue2 = alternate.updateQueue; if (queue1 === null) { if (queue2 === null) { // Neither fiber has an update queue. Create new ones. queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState); queue2 = alternate.updateQueue = createUpdateQueue( alternate.memoizedState, ); } else { // Only one fiber has an update queue. Clone to create a new one. queue1 = fiber.updateQueue = cloneUpdateQueue(queue2); } } else { if (queue2 === null) { // Only one fiber has an update queue. Clone to create a new one. queue2 = alternate.updateQueue = cloneUpdateQueue(queue1); } else { // Both owners have an update queue. } } } if (queue2 === null || queue1 === queue2) { // There's only a single  queue. appendUpdateToQueue(queue1, update); } else { // There are two queues. We need to append the update to both queues, While accounting for the persistent structure of the list -- we don't // want the same update to be added multiple times. if (queue1.lastUpdate === null || queue2.lastUpdate === null) { // One of the queues is not empty. We must add the update to both queues. appendUpdateToQueue(queue1, update); appendUpdateToQueue(queue2, update); } else { // Both queues are non-empty. The last update is the same in both lists, // because of structural sharing. So, only append to one of the lists. appendUpdateToQueue(queue1, update); // But we still need to update the `lastUpdate` pointer of queue2. queue2.lastUpdate = update; }}} /* 1) If queue.lastUpdate is null, set firstUpdate and lastUpdate to the latest update. */ function appendUpdateToQueue<State>(queue: appendUpdateToQueue: appendUpdateToQueue: appendUpdateToQueue: appendUpdateToQueue: appendUpdateToQueue: UpdateQueue<State>, update: Update<State>, ) { // Append the update to the end of the list. if (queue.lastUpdate === null) { // Queue is empty queue.firstUpdate = queue.lastUpdate = update; } else { queue.lastUpdate.next = update; queue.lastUpdate = update; }}Copy the code

Both queues are a persistent linked list structure. To schedule updates, we insert it at the end of each queue. In the persistent linked list, each queue has a pointer to the first Update that has not yet been processed. Because it is always working on work-in-Progress, its pointer position is always equal to or greater than that of the current queue. Pointers to current Update are replaced by work-in-progress updates only during commit.

For example:

Current pointer: A - B - C - D - E - F Work-in-progress pointer: D-e-f ^ As shown by the arrow above: work-in-progress processes updates more than currentCopy the code

The main reason for adding two queues is to avoid losing updates. For example, if we only add updates to the work-in-progress queue, some updates might be lost when work-in-progress restarts by cloning from the current queue. Similarly, if we only add updates to the current queue, updates will also be lost when an existing work-in-progress queue commits and replaces the current queue. By adding updates to both queues, we can ensure that updates are always part of the next work-in-progress. And because the work-in-progress queue will become the current queue once committed, there is no risk that the same update will be applied twice.

Updates are not stored by priority, but by insertion order, and the latest update is always added to the end of the list. However, priorities still matter. When updates are processed in the Render phase, only the higher-priority updates get the results. If an update is ignored due to insufficient priority, it remains in the queue waiting for the lower-priority Render phase to be processed. Regardless of priority, all updates are stored in the queue. This means that high-priority UPDATES can sometimes be processed twice with two different priorities. We also keep a trace of the base state, which represents the state in the queue before updatequue. FitstUpdate was applied.

For example:

Given a "baseSate" and the following updates A1-B2-C1-D2 numbers indicate the priority. React will process these updates through two separate render phases: first render, 1 priority: Base state: "Updates: [A1, C1] Result state:" B. Skipped over. B. Skipped over. B. Skipped over. B. Skipped over. B. Skipped over. [B2, C1, D2] <- C1 was rebased on top of B2 Result state: 'ABCD'Copy the code

renderRoot

File location: the react – the reconciler/SRC/ReactFiberWorkLoop. Js

The reconciliation process always starts with renderRoot, and the function stack looks like scheduleWork –> scheduleCallbackForRoot –> renderRoot

RenderRoot’s main code logic is as follows:

function renderRoot( root: FiberRoot, expirationTime: ExpirationTime, isSync: boolean, ) | null { do { if (isSync) { workLoopSync(); } else { workLoop(); } } while (true); } / * all fiber node processing * / function in workLoop workLoop () {/ / Perform the work until the Scheduler asks us to yield the while (workInProgress ! == null && ! shouldYield()) { workInProgress = performUnitOfWork(workInProgress); } } function workLoopSync() { // Already timed out, so perform work without checking if we need to yield. while (workInProgress ! == null) { workInProgress = performUnitOfWork(workInProgress); }}Copy the code

performUnitOfWork

The function call stack is as follows:

performUnitOfWork –> beginWork –> updateClassComponent –> finishedComponent –> completeUnitOfWork

The Reconciliation algorithm always traverses the tree from the topmost HostRootfiber node. React, however, skips fiber nodes that have already been processed until it finds nodes that haven’t finished work yet. For example, if setState is called deep in the component tree, React will start at the top but quickly skip the parent node until it reaches the component that called the setState method.

All fiber nodes are processed in workLoop.

When React traverses the Fibers tree, it uses workInProgress to know if any other Fiber nodes have unfinished work. After the current fiber is processed, workInProgress will contain a reference to the next Fiber node in the tree, if null, React exits the work loop and is ready to commit the change.

The depth-first search algorithm is adopted in the whole process, which gives priority to finding whether workInProgress has child nodes. If it does, the child nodes of the child node are searched until the searched child node is null, and the sibling nodes of the child node are returned to continue searching. Again, continue to find the children of the children of that sibling, and so on.

For details, you can click the following link: stackblitz.com/edit/js-ntq…

The core code is as follows:

File location: the react – the reconciler/SRC/ReactFiberWorkLoop. Js

function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
  // 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, renderExpirationTime);

  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(unitOfWork);
  }

  return next;
}
Copy the code

beginWork

File location: the react – the reconciler/SRC/ReactFiberBeginWork. Js

The beginWork function determines the work type of the fiber node through workinProgress. tag, and returns the function corresponding to the corresponding tag to perform the work. If workinProgress. tag is ClassComponent, execute updateClassComponent and finishClassComponent. Finally, return workinprogress.child.

The main code logic is as follows:

function beginWork( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ): Fiber | null { switch (workInProgress.tag) { case FunctionComponent: return updateFunctionComponent( current, workInProgress, Component, resolvedProps, renderExpirationTime, ); case ClassComponent: return updateClassComponent( current, workInProgress, Component, resolvedProps, renderExpirationTime, ) case HostRoot: . case HostComponent: ... case HostText: ... Function updateClassComponent(current: updateClassComponent) Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpirationTime: ExpirationTime, ) { const nextUnitOfWork = finishClassComponent( current, workInProgress, Component, shouldUpdate, hasContext, renderExpirationTime, ); Return nextUnitOfWork} /* Return the child of workInProgress */ function finishClassComponent(current: Fiber | null, workInProgress: Fiber, Component: any, shouldUpdate: boolean, hasContext: boolean, renderExpirationTime: ExpirationTime, ) { return workInProgress.child; }Copy the code

Here are some important operations performed in functions in order of execution:

  • Call UNSAFE_componentWillReceiveProps () hook (obsolete)
  • Process the UPDATE in update Ue and generate the new state
  • Call getDerivedStateFromProps with the new state and get the result
  • Call shouldComponentUpdate to determine if the component is updated; If false, the entire render process is skipped, including calling Render on the component and its children. Otherwise, continue with the update
  • Calling UNSAFE_componentWillUpdate(deprecated)
  • Add an effect to trigger the componentDidUpdate lifecycle hook (although an effect calling componentDidUpdate is added in the Render phase, this method will be executed in the Commit phase)
  • Update the state and props on the component instance

Once that’s done, React enters the finishClassComponent. Here React calls the Render method on the component instance and applies its diff algorithm to the child components. ** Notice the change in effectTag and updateQueue values of the workInProgress node at this point. The effectTag value is no longer 0, but the corresponding effect value. UpdateQueue’s Base Estate contains the payload value for Effect. These changes are made during the subsequent COMMIT phase, when React is used to do what needs to be done on the node.

The Render phase is complete, and it can now assign the completed alternate tree to the finishedWork property on fiberRoot. This is a new tree that needs to be committed to the screen. It can be processed immediately after the Render phase or after the browser has given up time.

The following code looks like this:

function renderRoot() {
    // We now have a consistent tree. The next step is either to commit it, or, if
  // something suspended, wait to commit it after a timeout.
  stopFinishedWorkLoopTimer();

  root.finishedWork = root.current.alternate;
  root.finishedExpirationTime = expirationTime;
}
Copy the code

completeUnitOfWork

File location: the react – the reconciler/SRC/completeUnitOfWork. Js

React builds an effect-list in the function completeUnitOfWork.

Alternate; workInProgress.return; workinProgress. sibling; if there are sibling nodes, return. Otherwise, return the parent node.

The main source code is as follows:

function completeUnitOfWork(unitOfWork: Fiber): Fiber | null { // 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. workInProgress = unitOfWork; do { const current = workInProgress.alternate; const returnFiber = workInProgress.return; // Append all the effects of the subtree and this fiber onto the effect // List of the parent. The completion order of the children affects the // side-effect order. if (returnFiber.firstEffect === null) { returnFiber.firstEffect = workInProgress.firstEffect; } if (workInProgress.lastEffect ! == null) { if (returnFiber.lastEffect ! == null) { returnFiber.lastEffect.nextEffect = workInProgress.firstEffect; } returnFiber.lastEffect = workInProgress.lastEffect; } if (returnFiber.lastEffect ! == null) { returnFiber.lastEffect.nextEffect = workInProgress; } else { returnFiber.firstEffect = workInProgress; } returnFiber.lastEffect = workInProgress; /* depth-first search algorithm part */ const siblingFiber = workinprogress.sibling; if (siblingFiber ! == null) { // If there is more work to do in this returnFiber, do that next. return siblingFiber; } // Otherwise, return to the parent workInProgress = returnFiber; } while (workInProgress ! == null); }Copy the code

The commit phase

The COMMIT phase is where React updates the DOM and calls the pre-Commit Phase and Commit Phase lifecycle methods. To do this, it iterates through the Effect List built in the previous Render phase and applies them.

Note that unlike the Render phase, the commit phase execution is always synchronous.

commitRootImpl

The COMMIT phase is divided into several sub-phases. Each sub-stage is passed effect list separately. All mutation effects are executed before all Layout effects.

Is divided into the following three sub-stages:

  • before mutation
  • mutation phase
  • layout phase

The first substage is the “before mutation” stage. Before React Mutate, React uses this phase to read the state of the host tree. This is where the getSnapshotBeforeUpdate lifecycle is called.

The next subphase is the “mutation phase,” where React changes the host tree. When the phase execution ends, the work-in-Progress tree becomes a Current tree, which must occur after the mutation phase so that it remains the previous Current tree for the duration of the componentWillUnmount life cycle. However, it should also occur before the “Layout Phase” phase so that during the componentDidMount/Update lifecycle, the Current tree is a completed work operation.

The next subphase is the “Layout Phase,” where the host tree has been changed and effects has been called. Life cycles such as componentDidMount/Update are executed in this phase.

File location: the react – the reconciler/SRC/ReactFiberWorkLoop. Js

The main source code is as follows:

function commitRootImpl(root) { // Get the list of effects. let firstEffect; if (finishedWork.effectTag > PerformedWork) { // A fiber's effect list consists only of its children, not itself. So if // the root has an effect, we need to add it to the end of the list. The // resulting list is the set that would belong to the root's parent, if it // had one; that is, all the effects in the tree including the root. if (finishedWork.lastEffect ! == null) { finishedWork.lastEffect.nextEffect = finishedWork; firstEffect = finishedWork.firstEffect; } else { firstEffect = finishedWork; } } else { // There is no effect on the root. firstEffect = finishedWork.firstEffect; } if (firstEffect ! == null) { // The commit phase is broken into several sub-phases. We do a separate pass // of the effect list for each phase: all mutation effects come before all // layout effects, and so on. // The first phase a "before mutation" phase. We use this phase to read the // state of the host tree right before we mutate it. This is where // getSnapshotBeforeUpdate is called. do { try { commitBeforeMutationEffects(); } catch (error) { nextEffect = nextEffect.nextEffect; } } while (nextEffect ! == null); // The next phase is the mutation phase, where we mutate the host tree. nextEffect = firstEffect; do { try { commitMutationEffects(); } catch (error) { nextEffect = nextEffect.nextEffect; } } while (nextEffect ! == null); // The work-in-progress tree is now the current tree. This must come after // the mutation phase, so that the previous tree is still current during // componentWillUnmount, but before the layout phase, so that the finished // work is current during componentDidMount/Update. root.current = finishedWork; // The next phase is the layout phase, where we call effects that read // the host tree after it's been mutated. The idiomatic use case for this is // layout, but class component lifecycles also fire here for legacy reasons. nextEffect = firstEffect; do { try { commitLayoutEffects(root, expirationTime); } catch (error) { invariant(nextEffect ! == null, 'Should be working on an effect.'); captureCommitPhaseError(nextEffect, error); nextEffect = nextEffect.nextEffect; } } while (nextEffect ! == null); nextEffect = null; // Tell Scheduler to yield at the end of the frame, so the browser has an // opportunity to paint. requestPaint(); } else { // No effects. root.current = finishedWork; } onCommitRoot(finishedWork.stateNode, expirationTime); // If layout work was scheduled, flush it now. flushSyncCallbackQueue(); return null; }Copy the code

commitBeforeMutationLifeCycles

File location: the react – the reconciler/SRC/ReactFiberCommitWork. Js

The before mutation phase performs the logic. The function call stack: commitRootImpl — > commitBeforeMutationEffects – > commitBeforeMutationLifeCycles

The main code is as follows:

function commitBeforeMutationEffects() { while (nextEffect ! == null) { if ((nextEffect.effectTag & Snapshot) ! == NoEffect) { const current = nextEffect.alternate; commitBeforeMutationEffectOnFiber(current, nextEffect); } nextEffect = nextEffect.nextEffect; } } function commitBeforeMutationLifeCycles( current: Fiber | null, finishedWork: Fiber, ): void { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: { commitHookEffectList(UnmountSnapshot, NoHookEffect, finishedWork); return; } / * class component implementation instance. GetSnapshotBeforeUpdate * / case ClassComponent: { if (finishedWork.effectTag & Snapshot) { 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; } } return; } case HostRoot: case HostComponent: case HostText: case HostPortal: case IncompleteClassComponent: // Nothing to do for these component types return; default: { invariant( false, 'This unit of work tag should not have side-effects. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); }}}Copy the code

commitBeforeMutationLifeCycles

File location: the react – the reconciler/SRC/ReactFiberWorkLoop. Js

Mutation Phase Phase execution logic. Function call stack: commitRootImpl –> commitMutationEffects –> commitPlacement or commitWork or commitDeletion, etc

The main code is as follows:

function commitMutationEffects() { // TODO: Should probably move the bulk of this function to commitWork. while (nextEffect ! == null) { const effectTag = nextEffect.effectTag; // The following switch statement is only concerned about placement, // updates, and deletions. To avoid needing to add a case for every possible // bitmap value, we remove the secondary effects from the effect tag and // switch on that value. let primaryEffectTag = effectTag & (Placement | Update | Deletion); switch (primaryEffectTag) { case Placement: { commitPlacement(nextEffect); // Clear the "placement" from effect tag so that we know that this is // inserted, before any life-cycles like componentDidMount gets called. // TODO: findDOMNode doesn't rely on this any more but isMounted does // and isMounted is deprecated anyway so we should be able to kill this. nextEffect.effectTag &= ~Placement; break; } case PlacementAndUpdate: { // Placement commitPlacement(nextEffect); // Clear the "placement" from effect tag so that we know that this is // inserted, before any life-cycles like componentDidMount gets called. nextEffect.effectTag &= ~Placement; // Update const current = nextEffect.alternate; commitWork(current, nextEffect); break; } case Update: { const current = nextEffect.alternate; commitWork(current, nextEffect); break; } case Deletion: { commitDeletion(nextEffect); break; }}}}Copy the code

commitLayoutEffects

File location: the react – the reconciler/SRC/ReactFiberCommitWork. Js

Layout Phase Phase execution logic. Function call stack: commitRootImpl –> commitLayoutEffects –> commitLifeCycles

The main code is as follows:

function commitLifeCycles( finishedRoot: FiberRoot, current: Fiber | null, finishedWork: Fiber, committedExpirationTime: ExpirationTime, ): void { switch (finishedWork.tag) { case FunctionComponent: case ForwardRef: case SimpleMemoComponent: { commitHookEffectList(UnmountLayout, MountLayout, finishedWork); break; } case ClassComponent: { const instance = finishedWork.stateNode; if (finishedWork.effectTag & Update) { 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) { // 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, committedExpirationTime, ); } return; } 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; } } commitUpdateQueue( finishedWork, updateQueue, instance, committedExpirationTime, ); } return; } 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.effectTag & Update) { const type = finishedWork.type; const props = finishedWork.memoizedProps; commitMount(instance, type, props, finishedWork); } return; } case HostText: { // We have no life-cycles associated with text. return; } case HostPortal: { // We have no life-cycles associated with portals. return; } case Profiler: { if (enableProfilerTimer) { const onRender = finishedWork.memoizedProps.onRender; if (enableSchedulerTracing) { onRender( finishedWork.memoizedProps.id, current === null ? 'mount' : 'update', finishedWork.actualDuration, finishedWork.treeBaseDuration, finishedWork.actualStartTime, getCommitTime(), finishedRoot.memoizedInteractions, ); } else { onRender( finishedWork.memoizedProps.id, current === null ? 'mount' : 'update', finishedWork.actualDuration, finishedWork.treeBaseDuration, finishedWork.actualStartTime, getCommitTime(), ); } } return; } case SuspenseComponent: case SuspenseListComponent: case IncompleteClassComponent: return; case EventComponent: { if (enableFlareAPI) { mountEventComponent(finishedWork.stateNode); } return; } default: { invariant( false, 'This unit of work tag should not have side-effects. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); }}}Copy the code

extension

Some other discussion about Fiber

About requestIdleCallback

The main thread usually has a short period of idle time between the two execution frames. RequestIdleCallback can call the idle period callback during this idle period to perform some low-priority work.

This was done on early React versions, but requestIdleCallback actually had some limitations and wasn’t implemented frequently enough to achieve smooth UI rendering, so the React team abandoned the requestIdleCallback usage and had to implement their own version, See the React source Packages/Scheduler section for details

Stack Reconciler and Fiber Reconciliation

Stack Reconciler is the reconciliation algorithm used in React V15 and previous versions.

The implementation of the Stack Reconciler uses a synchronous recursive model that relies on a built-in Stack to traverse the tree. It has some limitations, most notably the inability to decompose work into incremental units. We cannot pause Work on a particular component and continue execution later.

Note that under the Stack Reconciler, UPDATES to the DOM are synchronous, which means that during the virtual DOM comparison process, when an instance is found to be updated, the DOM operation is immediately executed.

So how does Fiber Reconciliation implement an algorithm that doesn’t need to recursively traverse the tree? It uses a single linked list tree traversal algorithm, which makes it possible to pause traversal and stop stack recursion.

Here’s Andrew from the React team:

If we relied only on the built-in call stack, it would work until the stack was empty, wouldn’t it be nice if we could interrupt the call stack at will and manipulate the stack frame manually? That’s the goal of React Fiber. Fiber is a reimplementation of the built-in stack, specifically for the React component. You can think of a fiber as a virtual stack frame.

So to solve this problem, React had to re-implement a tree traversal from a synchronous recursive model that relied on built-in stacks to an asynchronous model that included linked lists and Pointers. The advantage of reimplementing the stack is that stack frames can be kept in memory and executed at any time, which is critical to achieving scheduling goals.

reference

The facebook/react github.com/facebook/re…

In-depth Explanation of State and props Update In React medium.com/react-in-de…

In-depth Overview of the New Reconciliation Algorithm in React medium.com/react-in-de…

The How and Why on React’s Usage of Linked List in Fiber to Walk The Component’s tree medium.com/react-in-de…

“Effect – the list fiber chain table build process,” www.bilibili.com/video/av483…

“The react – fiber – architecture github.com/acdlite/rea…