preface

In this section, we use reactdom.render to render a simple function component to understand the initial rendering process and operation mechanism of React.

  1. Code examples:
import React from 'react';
import ReactDOM from 'react-dom';
function App() {
    return 'Hello React! '
}
ReactDOM.render(<App />.window.root);
Copy the code

2. Code debugging: go to the browser console-sources-react-dom file, search the render method and add a breakpoint inside it. Refresh the page to debug.

Before entering the render method, Babel parses and compiles JSX syntax elements and processes them into act-aware syntax and node information. In this example, the element data format in the Render method is:

{
    $$typeof: Symbol(react.element)
    key: null
    props: {}
    ref: null
    type: ƒ App()
}
Copy the code

Step 1: Create the root node of the application Fiber tree

function render(element, container, callback) {
    return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
}
Copy the code

In legacRenderSubtreeIngoContainer method first calls the legacCreateRootFromDOMContainer create the whole application (FiberRootNode) Fiber tree, And the Fiber node (HostRoot) corresponding to the root container:

function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
    let root = container._reactRootContainer; // Use _reactRootContainer to determine if this is the first render
    let fiberRoot;
    if(! root) { root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate); fiberRoot = root._internalRoot;// ...
        
        unbatchedUpdates(() = >{ updateContainer(children, fiberRoot, parentComponent, callback); }); }}Copy the code

In legacCreateRootFromDOMContainer involves the correlation function of the call stack level deeper, but the core is to perform createFiberRoot this method to create and return Fiber tree:

 function createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks) {
    const root = new FiberRootNode(containerInfo, tag, hydrate); // Create the Fiber application tree
    const uninitializedFiber = createHostRootFiber(tag); // Create the Fiber node corresponding to the root container
    root.current = uninitializedFiber;
    uninitializedFiber.stateNode = root;

    initializeUpdateQueue(uninitializedFiber); // Create updateQueue for fiber
    return root;
}
Copy the code

Step 2: Scheduler scheduling phase

The React workflow is divided into three phases:

  • Schedule: Sorts tasks with different priorities that are generated.
  • Render: Decide which views to update based on priority tasks.
  • Commit: Updates the view that needs to be changed to the view.

React now works in three modes:

  • Legacy mode: PassReactDOM.render(<App>, rootNode)Create, the current React default mode,
  • Blocking mode: Access is blockedReactDOM.createBlockingRoot(rootNode).render(<App />)Create, currently being tested as the first step in moving to Concurrent mode;
  • Concurrent mode: PassReactDOM.createRoot(rootNode).render(<App />)React will be the default mode when it stabilizes. This mode enables all new features.

In the schedule phase, tasks are scheduled mainly in Concurrent mode, while the Legacy mode we are currently using does not do much logical processing in this phase.

Created in the previous step legacRenderSubtreeIngoContainer Fiber, after the application is executed updateContainer ready to enter the React the first phase of the process: the scheduler (scheduling) :

unbatchedUpdates(() = > {
    updateContainer(children, fiberRoot, parentComponent, callback);
});

export function unbatchedUpdates(fn, a) {
    const prevExecutionContext = executionContext; // Get the last execution context
    executionContext &= ~BatchedContext;
    executionContext |= LegacyUnbatchedContext;
    try {
        return fn(a); // updateContainer
    } finally {
        executionContext = prevExecutionContext; // Restore execution stack}}Copy the code

It can be seen that the above code will execute unbatchedUpdates first to modify the current execution stack to LegacyUnbatchedContext (traditional synchronous mode). Since it is the initial rendering, it needs to render to the page as fast as possible, using synchronous mode.

One of the main things you do in updateContainer is create an update (each update corresponds to an update, in this case element) and call scheduleUpdateOnFiber to schedule:

export function updateContainer(element, container, parentComponent, callback) {
    // ...
    const update = createUpdate(eventTime, lane, suspenseConfig); // Create an update
    update.payload = {element};
    enqueueUpdate(current, update); // Add this update to fiber's Update queue. Each fiber has multiple Updates
    scheduleUpdateOnFiber(current, lane, eventTime); // The system starts to enter the schedule phase
}

function enqueueUpdate(fiber, update) {
    const updateQueue = fiber.updateQueue;
    const sharedQueue = updateQueue.shared; // { pending: null }
    const pending = sharedQueue.pending;
    if (pending === null) {
        update.next = update; // loop unidirectional list
    } else {
        update.next = pending.next;
        pending.next = update;
    }
    sharedQueue.pending = update;
}
Copy the code

In scheduleUpdateOnFiber, scheduling the initial rendering is easy (no scheduling is required) because it is the first synchronous rendering and the execution context is LegacyUnbatchedContext. So performSyncWorkOnRoot starts the loop to get each task into the Work phase (render and Commit phases).

function scheduleUpdateOnFiber(fiber, lane, eventTime) {
    // ...
    if (lane === SyncLane) {
        if( (executionContext & LegacyUnbatchedContext) ! == NoContext &&// Executing the unbatchedUpdates method on the initial render modifies the executionContext
            (executionContext & (RenderContext | CommitContext)) === NoContext // There is no render or commit phase yet
        ) {
            Every task is executed synchronously from The Root Root Fiber into the Render phase
            performSyncWorkOnRoot(root);
        } 
        // ...
    }
    // ...
}

export function performSyncWorkOnRoot(root) {
    // ...
    
    / / render phase
    renderRootSync(root, lanes);

    / / the commit phase
    commitRoot(root);
}
Copy the code

Step 3: render stage

function renderRootSync(root, lanes) {
    const prevExecutionContext = executionContext;
    executionContext |= RenderContext; // Change the current execution stack to Render phase
    if(workInProgressRoot ! == root || workInProgressRootRenderLanes ! == lanes) { prepareFreshStack(root, lanes); }do {
        try {
            workLoopSync();
            break;
        } catch(thrownValue) { handleError(root, thrownValue); }}while (true);

    executionContext = prevExecutionContext; // Restore execution stack
    After all tasks are executed in the workLoopSync in the Reconciler stage, clear the work information, and commitRoot starts with fiberRootNode instead of workInProgressRoot
    workInProgressRoot = null;
    workInProgressRootRenderLanes = NoLanes;
}
Copy the code

The prepareFreshStack method is first called in renderRootSync to initialize the work information (a global variable used on the React internal workflow) :

function prepareFreshStack(root, lanes) {
    root.finishedWork = null;
    root.finishedLanes = NoLanes;

    workInProgressRoot = root;
    // Create an alternate protocol based on HostRoot Fiber as the task of the current work
    workInProgress = createWorkInProgress(root.current, null);
    
    // ...
}

function createWorkInProgress(current, pendingProps) {
    let workInProgress = current.alternate;
    if (workInProgress === null) {
        // Create a Fiber Node based on current
        workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
        workInProgress.elementType = current.elementType;
        workInProgress.type = current.type;
        workInProgress.stateNode = current.stateNode;

        workInProgress.alternate = current; // Duplex protocol
        current.alternate = workInProgress;
    } else {
        workInProgress.pendingProps = pendingProps;
        workInProgress.type = current.type;
        workInProgress.effectTag = NoEffect;
        workInProgress.nextEffect = null;
        workInProgress.firstEffect = null;
        workInProgress.lastEffect = null;
    }
    // ...
    return workInProgress;
}
Copy the code

Then loop performUnitOfWork in workLoopSync to handle each task (one Fiber node for each task) :

function workLoopSync() {
    while(workInProgress ! = =null) { performUnitOfWork(workInProgress); }}function performUnitOfWork(unitOfWork) { // Unit of work --
    const current = unitOfWork.alternate;
    // render - render phase
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    unitOfWork.memoizedProps = unitOfWork.pendingProps; / / update the props
    
    // render - return stage (enter the return stage when the node has no child node, or the child has already been processed)
    if (next === null) {
        completeUnitOfWork(unitOfWork);
    } else{ workInProgress = next; }}Copy the code

The work of formunitofwork can be divided into two parts: the recursion stage (beginWork) and the return stage (completeUnitOfWork).

The beginWork is first called to compare the current node in the tree with the current working node and return its own child (if any).

If there are children (the next value exists), then performUnitOfWork is re-entered and beginWork is executed. If not, completeUnitOfWork is called to put the current node into the regression phase.

BeginWork recursive phase

The work of the beginWork phase is to pass in the current Fiber node, compare it with the nodes in the render tree (old nodes) (Diff), and create sub-fiber nodes. Due to the large number of node types, it is divided into multiple cases and handed over to different methods to deal with.

export function beginWork(current, workInProgress, renderLanes) {
    if(current ! = =null) { / / update phase
        / / beginWork first determines whether can reuse node, if can reuse, perform bailoutOnAlreadyFinishedWork method
    }
    
    switch (workInProgress.tag) {
        case HostRoot: / / the root node
            return updateHostRoot(current, workInProgress, renderLanes);
        case HostComponent: // Native node
            return updateHostComponent(current, workInProgress, renderLanes);
        case HostText: // Text node
            return updateHostText(current, workInProgress);
        case IndeterminateComponent: { // Function components mount with this tag, and change the tag to FunctionComponent after beginWork
            // Why do function components mount in this function and not in FunctionComponent?
            return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
        }
        case FunctionComponent: { // The function executes the logic s here in the update phase
            // ...}}}Copy the code

Beginwork-hostroot Indicates the processing of the beginWork node

The first HostRoot node to go to begin is the HostRoot root node, and the child node (the first parameter passed to the reactdom.render method) is taken from HostRoot’s updateQueue updateQueue. The reconCilechildren is called to create and return child nodes.

function updateHostRoot(current, workInProgress, renderLanes) {
    const nextProps = workInProgress.pendingProps;
    const prevState = workInProgress.memoizedState;
    constprevChildren = prevState ! = =null ? prevState.element : null;
    
    // clone current. UpdateQueue to workInProgress
    cloneUpdateQueue(current, workInProgress);
    / / from workInProgress. Get the update in the updateQueue. Content (child nodes), assigned to workInProgress. MemoizedState
    processUpdateQueue(workInProgress, nextProps, null, renderLanes);
    const nextState = workInProgress.memoizedState;
    const nextChildren = nextState.element;
    
    // If two bytes are the same, reuse the node, no need to create a new fiber
    if (nextChildren === prevChildren) {
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
    
    // Otherwise start creating sub-fiber
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
    return workInProgress.child;
}
Copy the code

beginWork – reconcileChildren

The reconcileChildren reconcileChildren is processed differently according to whether or not the Current has a value (mount/update) (the difference is whether or not the second parameter Current.child has a value), and ultimately creates a Child node for the current Fiber.

MountChildFibers and reconcileChildFibers point to the same method, and the mount and Update are internally distinguished by the shouldTrackSideEffects variable.

function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
    if (current === null) { // New component not yet rendered, first rendered
        workInProgress.child = 
            mountChildFibers(workInProgress, null, nextChildren, renderLanes);
    } else { // Update render (HostRoot will enter here in initial render, other nodes will enter here only when updating render)workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes); }}Copy the code

In reconcileChildFibers, the reconcileChildFibers are processed in different ways, including fragments, single nodes, multiple nodes, and plain text nodes, depending on the types of nodes in the nextChildren reconcileChildFibers.

function reconcileChildFibers(returnFiber, currentFirstChild, newChild, lanes) {
    // If the Child fiber type is fragment, skip it and process its children
    const isUnkeyedTopLevelFragment = typeof newChild === 'object'&& newChild ! = =null &&
        newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null;
    if (isUnkeyedTopLevelFragment) {
        newChild = newChild.props.children;
    }
    const isObject = typeof newChild === 'object'&& newChild ! = =null;
    if (isObject) { // If it is an object, it means that there is only one child element, which is handled by Single (regardless of sibling).
        switch (newChild.$$typeof) {
            case REACT_ELEMENT_TYPE:
                return placeSingleChild(
                    reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes),
                );
            // ...}}if (typeof newChild === 'string' || typeof newChild === 'number') {
        return placeSingleChild(
            reconcileSingleTextNode(returnFiber, currentFirstChild, ' ' + newChild, lanes),
        );
    }
    if (isArray(newChild)) {
        return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
    }

    // All other cases are empty. Remove all children from the parent node, making it an empty element
    return deleteRemainingChildren(returnFiber, currentFirstChild);
}
Copy the code

If the Child is the reconcileSingleElement’s child node, which is a non-text node, it goes into the reconcileSingleElement for processing (single-node Diff). Diff or new node is determined by whether the old child has a value.

function reconcileSingleElement(returnFiber, currentFirstChild, element, lanes) {
    const key = element.key;
    let child = currentFirstChild;
    
    while(child ! = =null) {
        // If the value is different, delete the node
        if (child.key === key) {
            // the key is the same
            switch (child.tag) {
                // ...
                default: {
                    // If the type is the same, it can be reused
                    if (child.elementType === element.type) {
                        // The same type indicates that the same node was found. Since this is a single-node process, the other sibling nodes on the view should be marked for deletion
                        deleteRemainingChildren(returnFiber, child.sibling);
                        const existing = useFiber(child, element.props);
                        existing.ref = coerceRef(returnFiber, child, element);
                        existing.return = returnFiber;
                        return existing;
                    }
                    // If type is different, the loop is broken
                    break; }}// Same key but different type
            // Mark this fiber and its sibling fiber as deleted, why do we want to handle Sibling?
            // Since there may be multiple child nodes on the view, there is only one single node in this update, which needs to be deleted
            deleteRemainingChildren(returnFiber, child);
            break;
        } else {
            // Mark the fiber as deleted
            deleteChild(returnFiber, child);
        }
        // There is only one new node at the moment, considering that the sibling of the old node in the view may be a multi-node,
        // Compare each old Sibling node in turn to see if it can be reused, so we also need to process sibling
        child = child.sibling;
    }

    // If a child node is added, or the old node is found to be unreusable after Diff, create a new node here
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
}
Copy the code

After the reconcileSingleElement is executed, the reconcileSingleElement returns to the child node created/reused and enters the placeSingleChild method as a parameter. In the case of the Update phase, The fiber operation node type is marked as Placement (add). For this example, HostRoot adds a Placement identifier to its newFiber (App function component) :

function placeSingleChild(newFiber) {
    // shouldTrackSideEffects exists to indicate the update phase
    if (shouldTrackSideEffects && newFiber.alternate === null) {
        newFiber.effectTag = Placement;
    }
    return newFiber;
}
Copy the code

If the child node is a text node, as in our example here: the function component returns a plain text string: Hello React! , so the reconcileChildFibers are dealt with using the reconcileSingleTextNode method:

function reconcileSingleTextNode(returnFiber, currentFirstChild, textContent, lanes) {
    // If the current fiber node corresponding to the child node exists and is also a text node, it is reused
    if(currentFirstChild ! = =null && currentFirstChild.tag === HostText) {
        deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
        const existing = useFiber(currentFirstChild, textContent);
        existing.return = returnFiber;
        return existing;
    }
    // In the update phase, the existing content in the parent node is deleted. In the mount phase, the method does not delete the content
    deleteRemainingChildren(returnFiber, currentFirstChild);
    const created = createFiberFromText(textContent, returnFiber.mode, lanes); // Create a text node
    created.return = returnFiber; // Build relationships
    return created;
}
Copy the code

The internal implementation is simple: if the node can be reused, return to the reused node, otherwise create a new text Fiber node.

BeginWork – Function component processing

For function components, in creating the corresponding fiber phase, the tag type assignment for IndeterminateComponent components (not sure), so in beginWork approach will hit mountIndeterminateComponent method logic.

MountIndeterminateComponent to class component and function component is the most different processing, here we skip class components, focus on the treatment of functional components.

function mountIndeterminateComponent(_current, workInProgress, Component, renderLanes) {
    let value = renderWithHooks(null, workInProgress, Component, props, context, renderLanes); // Returns the result of the function execution
    // For this example, the function component is HostRoot's child. In placeSingleChild, set effectTag to Update (2).
    // After the function component completes its work, it will mark effectTag or equal to PerformedWork 1
    workInProgress.effectTag |= PerformedWork; // PerformedWork value is 1

    // Omit the class component and look only at the processing logic of the function component
    workInProgress.tag = FunctionComponent;
    reconcileChildren(null, workInProgress, value, renderLanes);
    return workInProgress.child;
}
Copy the code

As you can see, the renderWithHooks method takes the function component as a parameter and passes in the return value as nextChild, where the value is the result of the return from the reconcileChildren function component. Let’s look at how function components are executed inside renderWithHooks.

function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
    currentlyRenderingFiber = workInProgress; // Save the fiber corresponding to the current function component, which will be used later when executing the hooksAPI inside the function

    // Determine whether hooks use mount or update mode
    ReactCurrentDispatcher.current =
            current === null || current.memoizedState === null
                ? HooksDispatcherOnMount
                : HooksDispatcherOnUpdate;

    let children = Component(props, secondArg);

    ReactCurrentDispatcher.current = ContextOnlyDispatcher;

    currentlyRenderingFiber = null;
    // Reset the hooks that perform work in the function component to make room for the next function component.
    currentHook = null;
    workInProgressHook = null;

    return children;
}
Copy the code

Mount or update to get the different Hooks execution methods, ReactCurrentDispatcher. This is where the Hooks logic comes from.

Then execute Component to call the function Component. When the function completes, it executes the internal Hooks and restores ReactCurrentDispatcher to its original value.

Beginwork-hosttext Indicates the processing of text nodes

In this case, the function component returns a plain text string: Hello React! Text Fiber will be created, and the beginWork function will be entered again. Since there is no child for the Text node, null will be returned for processing the Text node.

function updateHostText(current, workInProgress) {
    // Since it has no children, it doesn't need to proceed and returns null, putting the text node into the completeUnitOfWork stage
    return null;
}
Copy the code

CompleteUnitOfWork belongs to the stage

And then for our example, Hello React! The text node has no children, so it will enter the return stage. Let’s first look at the core function of this method:

  • Call completeWork to create a real DOM node for the current Fiber node;
  • Add the list of effects saved on the current Fiber node to the parent node;
  • If there are siblings, the SIBLING node is entered into the BEGIN recursive phase as workInProgress.
  • If there are no siblings, return to the parent node and let the parent node enter the complete return phase. After that, look for sibling of the parent node and enter the Beigin recursive phase.
export function completeUnitOfWork(unitOfWork) {
    // Complete the current unit of work, then go to the next unit sibling, if there is no sibling, return to the parent node to find the uncle
    let completedWork = unitOfWork;
    do {
        const current = completedWork.alternate;
        const returnFiber = completedWork.return;

        // This object needs to be modified (effectTag === placement, delete, update)
        if ((completedWork.effectTag & Incomplete) === NoEffect) {
            let next = completeWork(current, completedWork, subtreeRenderLanes);
            if(next ! = =null) { // Take on a new task
                workInProgress = next;
                return;
            }

            if(returnFiber ! = =null && (returnFiber.effectTag & Incomplete) === NoEffect) {
                // add effect, the child node stored on the current node, to the parent node
                if (returnFiber.firstEffect === null) {
                    returnFiber.firstEffect = completedWork.firstEffect;
                }
                if(completedWork.lastEffect ! = =null) {
                    if(returnFiber.lastEffect ! = =null) {
                        returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
                    }
                    returnFiber.lastEffect = completedWork.lastEffect;
                }

                // effect is added to the parent node
                const effectTag = completedWork.effectTag;
                if (effectTag > PerformedWork) { // Currently fiber has DOM update and other operations
                    if(returnFiber.lastEffect ! = =null) {
                        returnFiber.lastEffect.nextEffect = completedWork;
                    } else{ returnFiber.firstEffect = completedWork; } returnFiber.lastEffect = completedWork; }}}else {
            // TODO...
        }

        const siblingFiber = completedWork.sibling;
        if(siblingFiber ! = =null) { // sibling exists, let sibling enter begin phase
            workInProgress = siblingFiber;
            return;
        }
        completedWork = returnFiber;
        workInProgress = completedWork; // Update workInProgress to null
    } while(completedWork ! = =null);
}
Copy the code

CompleteWork mainly deals with root nodes, class components, native nodes, text nodes and other types of elements. Function components and other types are not handled:

function completeWork(current, workInProgress, renderLanes) {
    const newProps = workInProgress.pendingProps;
    switch (workInProgress.tag) {
        case IndeterminateComponent:
        case LazyComponent:
        case SimpleMemoComponent:
        case FunctionComponent:
        case ForwardRef:
        case Fragment:
        case Mode:
        case Profiler:
        case ContextConsumer:
        case MemoComponent:
            return null;
        case ClassComponent: {
            // ...
        }
        case HostRoot: {
            // ...
        }
        case HostComponent: {
            // ...
        }
        case HostText: {
            // ...}}}Copy the code

CompleteUnitOfWork – HostText Processing of the text node

If an old node exists and needs to be updated after comparison, updateHostText is called to mark the node effect as Update. Otherwise, create a new node.

case HostText: {
    const newText = newProps;
    if(current && workInProgress.stateNode ! =null) { // Update text operations
        const oldText = current.memoizedProps;
        updateHostText(current, workInProgress, oldText, newText); // Workinprogress. effectTag = Update
    } else {
        const rootContainerInstance = getRootHostContainer();
        const currentHostContext = getHostContext();
        workInProgress.stateNode = createTextInstance(newText, rootContainerInstance, currentHostContext, workInProgress);
    }
    return null;
}

function updateHostText(current, workInProgress, oldText, newText) {
    if(oldText ! == newText) markUpdate(workInProgress); }function markUpdate(workInProgress) {
    workInProgress.effectTag |= Update;
}

function createTextInstance(text, rootContainerInstance, hostContext, internalInstanceHandle) {
    const textNode = createTextNode(text, rootContainerInstance);
    textNode[internalInstanceKey] = internalInstanceHandle; // Add textFiber to the text node
    return textNode;
}
Copy the code

CompleteUnitOfWork – Function component processing

Although the function component does not need to create a node and returns directly from the completeWork element, in this case the function component is the root element of HostRoot and its effectTag is 3. So the completeUnitOfWork method adds its effect to the parent node’s effectList (HostRoot) :

const effectTag = completedWork.effectTag;
if (effectTag > PerformedWork) { // effectTag > 1 indicates an effect update operation
    if(returnFiber.lastEffect ! = =null) {
        returnFiber.lastEffect.nextEffect = completedWork;
    } else {
        returnFiber.firstEffect = completedWork;
    }
    returnFiber.lastEffect = completedWork;
}
// Since there is no effect on HostRoot, the above code is equivalent to:
// returnFiber.firstEffect = returnFiber.lastEffect = completedWork; // Add the fiber node to the effectList
Copy the code

CompleteUnitOfWork – HostRoot Processing of the root node

Since the real DOM corresponding to HostRoot is our container node ID =root, there is no need to create DOM. Here, we mainly clear the call stack and mark the effect of the root node as: Snapshot (256).

case HostRoot: {
    // Remove workInProgress from the execution stack
    popHostContainer(workInProgress);
    popTopLevelLegacyContextObject(workInProgress);
    const fiberRoot = workInProgress.stateNode;
    // When hostRoot enters the completeUnitOfWork phase, the render phase is about to end, and the context object information is updated
    if (fiberRoot.pendingContext) {
        fiberRoot.context = fiberRoot.pendingContext;
        fiberRoot.pendingContext = null;
    }
    
    Plan to clear the effect of this container at the start of the next commit.
    // Since this is the first rendering, the effectTag of the root node is set to Snapshot, indicating that the root node is also updated.
    / / and in commitRoot commitBeforeMutationEffects stage, will enter the processing method of the Snapshot is used to clear the root container for all the child nodes of the content,
    // Make sure the container node has no content so that all byte points can be mounted to the container node later
    workInProgress.effectTag |= Snapshot;
            
    updateHostContainer(workInProgress); // This is an empty method
    return null;
}
Copy the code

Step 4: Commit phase

The RenderRootSync method is executed synchronously in performSyncWorkOnRoot to process each fiber. After the Render phase is complete, the DOM node is rendered to the view in the Commit phase.

export function performSyncWorkOnRoot(root) {
    renderRootSync(root, lanes); // Render phase is executed synchronously

    const finishedWork = root.current.alternate; // workInProcess HostRoot Fiber
    // The EffectList is executed against it in commitRoot, iterating through each effect and making updates
    // (effectList is formed from the smallest child node to the topmost element that needs to be updated)
    root.finishedWork = finishedWork;
    root.finishedLanes = lanes;
    commitRoot(root);

    return null;
}

export function commitRoot(root) {
    const renderPriorityLevel = getCurrentPriorityLevel();
    / / used here ImmediateSchedulerPriority highest priority to perform commitRoot
    runWithPriority(
        ImmediateSchedulerPriority,
        commitRootImpl.bind(null, root, renderPriorityLevel), 
    );
    return null;
}
Copy the code

Commit processing is performed in commitRoot based on priority, but the core logic is in the commitRootImpl method.

The logic in commitRootImpl is more complex, dividing the COMMIT phase into three phases:

  • Some initial processing will be done before going into three phases…
  • Before Mutation phase (Before DOM operation)
  • Mutation stage (DOM operation performed)
  • Layout stage (after DOM manipulation)
  • After three stages, do some processing…

Commit – Processing before entering the three sub-phases

In the COMMIT phase, the nodes to be updated are processed based on effectLists. So Before entering the Before Mutation phase, the effectList is processed and the first node to be updated is retrieved: FirstEffect appends the root to the end of the effectList if there is an effectTag on the root.

function commitRootImpl(root, renderPriorityLevel) {    
    // Retrieve the work tree completed in the Render phase: finishedWork, which holds the effectList to be submitted for this Render
    const finishedWork = root.finishedWork;

    // Reset variable information on the root node
    root.finishedWork = null;
    root.finishedLanes = NoLanes;
    root.callbackNode = null;
    root.callbackId = NoLanes;

    // If there is an effect on the root node, add it to the end of the effectList (effectTag: Snapshot=256)
    // Then fetch the first node on the effectList to start updating the node
    let firstEffect;
    if (finishedWork.effectTag > PerformedWork) { // Update operation exists in rootFiber
        if(finishedWork.lastEffect ! = =null) {
            finishedWork.lastEffect.nextEffect = finishedWork; // Add the root node to the end of the effectList during initial rendering to mount the view on the container node
            firstEffect = finishedWork.firstEffect;
        } else{ firstEffect = finishedWork; }}else {
        // The root node has no effectTag
        firstEffect = finishedWork.firstEffect;
    }
 
    / /... Three phases of code processing.
    
    / /... Processing after three stages
}
Copy the code

After getting firstEffect, we started processing each effect into the Before Mutation stage, Mutation stage and Layout stage.

function commitRootImpl(root, renderPriorityLevel) {
    / /... Three stages enter before processing
 
    if(firstEffect ! = =null) {
        // Set the current stack to commit
        executionContext |= CommitContext;

        // Mutation phase 1: Before Mutation phase 2
        nextEffect = firstEffect; // nextEffect is an all variable
        do {
            commitBeforeMutationEffects();
        } while(nextEffect ! = =null);
        
        // Mutation phase 2: Mutation phase 2
        nextEffect = firstEffect;
        do {
            commitMutationEffects(root, renderPriorityLevel);
        } while(nextEffect ! = =null);
        root.current = finishedWork; After the Mutation phase is complete, update to current (since the node is still rendered to the view)

        // Stage 3: Layout stage
        nextEffect = firstEffect;
        do {
            commitLayoutEffects(root, lanes);
        } while(nextEffect ! = =null);
        nextEffect = null; // Reset the global change
        executionContext = prevExecutionContext; // Restore execution stack
    } else {
        // No effects.
        root.current = finishedWork;
    }
    
    / /... The processing after the completion of the three phases
}
Copy the code

Commit-before Mutation phase

Can see from the above code, since firstEffect traverse effect in turn into commitBeforeMutationEffects to deal with. Methods Internal whole processing is divided into three parts:

  • Handle autoFocus and blur logic on DOM nodes;
  • Call the getSnapshotBeforeUpdate lifecycle hook function (this function is used to replace the previous ComponentWillXXX hook, which is executed during the Before Mutation phase under the COMMIT phase because it is synchronous, Not like those hooks called multiple times during the interruptible Render phase).
  • Schedule useEffect (call flushPassiveEffects to schedule useEffect asynchronously).
function commitBeforeMutationEffects() {
    while(nextEffect ! = =null) {
        const current = nextEffect.alternate;

        if(! shouldFireAfterActiveInstanceBlur && focusedInstanceHandle ! = =null) {
            / /... Focus blur Process the autoFocus and blur logic after DOM node rendering/deletion
        }

        const effectTag = nextEffect.effectTag;

        // Calling getSnapshotBeforeUpdate life cycle hook class component and first mounted HostRoot will enter here
        if((effectTag & Snapshot) ! == NoEffect) { commitBeforeMutationEffectOnFiber(current, nextEffect);// What HostRoot does is clean up the nodes in the container
        }
        / / scheduling useEffect
        if((effectTag & Passive) ! == NoEffect) {if(! rootDoesHavePassiveEffects) { rootDoesHavePassiveEffects =true;
                scheduleCallback(NormalSchedulerPriority, () = > {
                    / / triggers useEffect
                    flushPassiveEffects();
                    return null; }); } } nextEffect = nextEffect.nextEffect; }}Copy the code

Commit – Mutation phase

The Mutation phase is processed in the commitMutationEffects method, which renders the updates to the view. Internally, the node is treated differently based on the effectTag type:

  • Placement effect: Get the parent node, get the sibling node (for insertBefore, appendChild), and insert Fiber’s corresponding DOM node into the page parent node. There may be a constant search for parent or child nodes because the function component has no real node.
  • Update effect: Updates native node properties.
  • Deletion effect: Deletes a node.
function commitMutationEffects(root, renderPriorityLevel) {
    while(nextEffect ! = =null) {
        const effectTag = nextEffect.effectTag;
        / /... Omission of other tags, such as refTag

        // use the operator to quickly determine which category the effectTag belongs to. For example, if the effectTag of the function component is 3,3 & Placement(2) is 2, perform the insertion for the function component
        const primaryEffectTag = effectTag & (Placement | Update | Deletion | Hydrating);
        switch (primaryEffectTag) {
            / / insert the DOM
            case Placement: { / / 2
                commitPlacement(nextEffect);
                // Set the effectTag to 0 in the layout stage. Updates and deletions do not do this
                nextEffect.effectTag &= ~Placement;
                break;
            }
            / / update the DOM
            case Update: { / / 4
                const current = nextEffect.alternate;
                commitWork(current, nextEffect);
                break;
            }
            / / remove the DOM
            case Deletion: { / / 8
                commitDeletion(root, nextEffect, renderPriorityLevel);
                break; } } nextEffect = nextEffect.nextEffect; }}Copy the code

For the function component in this example, it has an effectTag of 3 and is handled as a Placement insert, calling commitPlacement.

The function component’s child node is added to the parent node (the function component is not a real DOM node). In this case, the React! It’s mounted to the view container, and the page sees the effect.

function commitPlacement(finishedWork) {
    // Get the parent DOM node. Where finishedWork is the incoming Fiber node
    const parentFiber = getHostParentFiber(finishedWork);
    let parent;
    let isContainer; // Whether it is a container
    // Parent DOM node
    const parentStateNode = parentFiber.stateNode;

    switch (parentFiber.tag) {
        case HostComponent:
            parent = parentStateNode; // Native DOM node
            isContainer = false;
            break;
        case HostRoot:
            parent = parentStateNode.containerInfo; // Container node
            isContainer = true;
            break;
        // ...
        default:
            break;
    }

    // Get the DOM sibling of the Fiber node
    const before = getHostSibling(finishedWork);
    Call parentNode.insertBefore or parentNode.appendChild to insert DOM depending on whether a DOM sibling exists.
    if (isContainer) {
        insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
    } else{ insertOrAppendPlacementNode(finishedWork, before, parent); }}Copy the code

Commit-layout phase

In the Layout phase, you go to the commitLayoutEffects method, which does two things:

  • Call the class component and Hooks functions (componentDidMount, componentDidUpdate, uselayoutEffect) if you can get the updated DOM in the Hooks function.
  • If the conditions are met, the ref node attribute is assigned.
function commitLayoutEffects(root, committedLanes) {
    while(nextEffect ! = =null) {
        const effectTag = nextEffect.effectTag;

        Call lifecycle hooks and hooks
        if (effectTag & (Update | Callback)) {
            const current = nextEffect.alternate;
            commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
        }

        // 2
        if(effectTag & Ref) { commitAttachRef(nextEffect); } nextEffect = nextEffect.nextEffect; }}Copy the code

Commit – Processing after the three subphases are complete

Finally, if no Effect Hook is used in the function component, the main work is initialization, starting with firstEffect, walking through each Effect node and removing the Effect flag from the Fiber node. So that the effect side effects can be recalculated on the next update.

The other point is to handle updates generated in the lifecycle callback, and new updates start a new render-commit process. (e.g. ComponentDidMount calls this.setState in the hook function).

function commitRootImpl(root, renderPriorityLevel) {    
    / /... Three stages prior to processing
 
    / /... Three phases of code processing.
    
    // 1. Initialize effectList to null
    const rootDidHavePassiveEffects = rootDoesHavePassiveEffects; // Effect is added after the root node is mounted
    if (rootDoesHavePassiveEffects) { // Handle Effect Hook
        rootDoesHavePassiveEffects = false;
        rootWithPendingPassiveEffects = root; // There is useEffect to handle, save root
        pendingPassiveEffectsLanes = lanes;
        pendingPassiveEffectsRenderPriority = renderPriorityLevel;
    } else { // Initialize the effectList to null
        nextEffect = firstEffect;
        while(nextEffect ! = =null) {
            const nextNextEffect = nextEffect.nextEffect;
            nextEffect.nextEffect = null;
            if(nextEffect.effectTag & Deletion) { detachFiberAfterEffects(nextEffect); } nextEffect = nextNextEffect; }}// Start a new update process
    ensureRootIsScheduled(root, now());
    flushSyncCallbackQueue();
}

function detachFiberAfterEffects(fiber) {
    fiber.sibling = null;
}
Copy the code

At the end

At this point, the initial rendering analysis of a simple function component is complete. Overall reading down the code is still very long, if there is a need to correct the article, welcome readers to put forward valuable comments.