How does reactdom.render concatenate render links? (below)

In the last lecture, starting from beginWork, we found out the creation link of Fiber node and the construction link of Fiber tree. This lecture will take completeWork as a clue to find the association between Fiber tree and DOM tree and explain the whole Render stage thoroughly. On this basis, the commit phase workflow will have a complete and transparent understanding of the render link triggered by Reactdom.render.

The experimental Demo of this lecture is consistent with the previous two lectures, and the code is as follows:

import React from "react"; import ReactDOM from "react-dom"; Function App() {return (<div className="App"> <div className="container"> <h1> </div> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);Copy the code

CompleteWork — Map a Fiber node to a DOM node

1) When the completeWork is called

First, let’s locate the completeWork in the call stack. In the call stack corresponding to the Demo, the first completeWork appears in the red box below:

One of the things we need to grasp from the diagram is that from performUnitOfWork to completeWork, there’s a call link that looks like this:

The work of the completeUnitOfWork is also critical, but look at the completeWork for a moment and think of the completeUnitOfWork simply as a “tool man” for making completeWork calls. CompleteUnitOfWork is called inside performUnitOfWork, so how does performUnitOfWork timing its call? Look directly at the source code (parsing in the comments) :

function performUnitOfWork(unitOfWork) { ...... Var current = unitOfWork. Alternate; var next; if (xxx) { ... Next = beginWork$1(current, unitOfWork, subtreeRenderLanes); . } else {next = beginWork$1(current, unitOfWork, subtreeRenderLanes); }... If (next === null) {// Call completeUnitOfWork completeUnitOfWork(unitOfWork); } else {// Update the current node to the newly created Fiber node workInProgress = next; }... }Copy the code

The information to be extracted from this source is: performUnitOfWork will try to call beginWork each time to create a child node of the current node. If the child node created is empty (which means that the current node does not have a child Fiber node), then the current node is a leaf node. According to the principle of depth-first traversal, when traversal reaches the leaf node, the “recursion” phase ends, followed by the “return” process. So in this case, completeUnitOfWork is called, executing the completeWork logic for the current node.

Next, break the Demo code’s completeWork to see which node is the first to go to completeWork. The result is as follows:

Obviously,The first node to enter the completeWork is h1, this is also consistent with the node relationship in the Fiber tree constructed in the last lecture, as shown in the figure below:

It can be seen from the figure that h1 will indeed be the first leaf node to be traversed according to the depth-first traversal principle. Let’s take the h1 as an example and see what the completeWork does around it.

2) How completeWork works

Again, extract the source structure and body logic of completeWork as follows (explained in the comments) :

Function completeWork(current, workInProgress, renderLanes) { Storage in newProps var newProps = workInProgress pendingProps; // Decide which logical switch (workinProgress.tag) {case...... : return null; case ClassComponent: { ..... } case HostRoot: { ...... Case HostComponent: {popHostContext(workInProgress); var rootContainerInstance = getRootHostContainer(); var type = workInProgress.type; // Check whether the current node exists. If (current! == null && workInProgress.stateNode ! = null) { updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance); if (current.ref ! == workInProgress.ref) { markRef$1(workInProgress); }} else {// return if (! newProps) { if (! (workInProgress.stateNode ! == null)) { { throw Error("We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue."); } } return null; Var currentHostContext = getHostContext(); // _wasdiuretic = popHydrationState(workInProgress). // if (_wasured-phase) { Please pay attention to the else inside the logic of the if (prepareToHydrateHostInstance (workInProgress rootContainerInstance, currentHostContext)) { markUpdate(workInProgress); }} else {// This step is crucial, Var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress); AppendAllChildren (instance, workInProgress, false, false) attempts to mount the DOM node created in the previous step to the DOM tree. // stateNode is used to store the DOM node corresponding to the current Fiber node. If (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) { markUpdate(workInProgress); }}... } return null; } case HostText: { ...... } case SuspenseComponent: { ...... } case HostPortal: ...... return null; case ContextProvider: ...... return null; . } { { throw Error("Unknown unit of work tag (" + workInProgress.tag + "). This error is likely caused by a bug in React.  Please file an issue."); }}}Copy the code

There are a few key points to grasp when trying to make sense of this completeWork logic.

  • The core logic of completeWork is a huge switch statement, in which completeWork enters the creation and processing logic of different DOM nodes based on the tag attributes of the workInProgress node.

  • In the Demo example, the tag attribute of the H1 node should correspond to HostComponent, which is the “native DOM element type.”

  • Current and workInProgress in completeWork correspond to the nodes on the left and right Fiber trees in the figure below:

  • The workInProgress tree represents the “currently render tree” and the current tree represents the “existing tree”.

  • The workInProgress and current nodes are connected with alternate properties. During the component mount phase, the current tree has only one rootFiber node and nothing else. Therefore, the current node corresponding to the h1 workInProgress node is null.

With the above premises, and then to read the above extracted source code, the idea may be much clearer.

After straightening out our thoughts, we will directly extract the knowledge points. There are a few things to understand about completeWork.

(1) Summarize completeWork’s work in one sentence: “Responsible for handling the mapping logic from Fiber nodes to DOM nodes.”

(2) There are three key actions inside completeWork:

  • Create a DOM node (CreateInstance)

  • Insert the DOM node into the DOM tree (AppendAllChildren)

  • Set properties for the DOM node (FinalizeInitialChildren)

(3) The created DOM node will be assigned to the stateNode attribute of the workInProgress node.

This means that when we want to locate a DOM node corresponding to Fiber, we can access its stateNode property. Here you can try to access the stateNode attribute of the runtime H1 node as shown below:

(4) Inserting DOM nodes into the DOM tree is done through the appendAllChildren function.

Insert the DOM node into the DOM tree, but actually mount the DOM node corresponding to the child Fiber node to the DOM node corresponding to the parent Fiber node. For example, in the Fiber tree constructed in this Demo, the parent of h1 node is div, so the DOM node corresponding to H1 should be mounted to the DOM node corresponding to div.

What if the parent DOM node does not already exist when appendAllChildren is executed?

For example, if the H1 node is the first to enter the completeWork, the DOM corresponding to its parent div does not yet exist. It doesn’t matter if the H1 DOM node doesn’t exist. After the h1 DOM node is created, it will exist as the stateNode attribute of the H1 Fiber node. When the parent div enters the appendAllChildren logic and looks down one by one to add its descendant nodes, the H1 is acquired by its parent DOM node ~

CompleteUnitOfWork — Starts the “big loop” of collecting EffectLists

The role of the completeUnitOfWork is to start a large loop in which the following three things are done repeatedly:

  • The completeWork is called for the current node that is passed in, and the completeWork does what we’ve already said, so there should be no objection to this step;

  • Insert the current node’s EffectList into its parent node’s EffectList;

  • Starting with the current node, loops through its siblings and their parents.

When traversing a sibling, the current call is returned, and the performUnitOfWork logic corresponding to the sibling is fired. When the parent node is traversed, it goes directly to the next loop, which repeats the logic of 1 and 2.

Step 1 needs no further explanation. Next, I will focus on the meanings of Step 2 and Step 3.

1) The principle of the completeUnitOfWork starting the next cycle

Before you understand the chain of side effects, you need to understand the principle that the completeUnitOfWork starts the next cycle, which is step 3. Step 3 The relevant source code is as follows (parsing in the comments) :

do { ...... // Get the sibling of the current node var siblingFiber = completedwork.sibling; If (siblingFiber! == null) {// Assign workInProgress to sibling of current node workInProgress = siblingFiber; // Return the ongoing completeUnitOfWork logic return return; } // If the sibling does not exist, completeWork is assigned to returnFiber, which is the parent of the current node completedWork = returnFiber; WorkInProgress = completedWork; workInProgress = completedWork; } while (completedWork ! == null);Copy the code

Step 3 is the end of the loop body and is executed after all the work related to the current node has been done.

After the current node is processed, it is natural to look for the next node that can be processed. We know that the current Fiber node enters the completeWork because there is “no recursion”, which means that the current Fiber node either has no child node or the completeWork of the child node has already been executed. Therefore, child nodes will not be considered for the next loop, which will only consider sibling nodes (siblingFiber) and parent nodes (returnFiber).

So ** why in the source code, the sibling node will return, the parent node will enter the next loop? ** The node relationship of the H1 node is used as an example. See the picture below:

According to the above analysis and figure, h1 node is the first leaf node touched in the recursive process, and also the first node traversed among its siblings. And the remaining two P nodes, at this point, have not been traversed, that is to say, beginWork has not even been performed.

Therefore, for sibling nodes of H1 node, the current first task is to start from beginWork, and the logic of completeWork can be executed only when beginWork “has no recursion”. The call to beginWork happens inside performUnitOfWork, so once the completeUnitOfWork recognizes that its sibling to the current node is not empty, it terminates the subsequent logic and falls back into the previous layer of performUnitOfWork.

Next, look at the parent div of H1: in the process of recursion down to H1, div must have been traversed, that is, the “beginWork” stage of div has been completed, and only the “return” stage is left to deal with. So for the parent, the completeUnitOfWork doesn’t hesitate to push it into the next loop, putting it into the completeWork’s logic.

It is worth noting that completeUnitOfWork processes sibling nodes and parent nodes in the following order: first check whether sibling nodes exist, and if so, first process sibling nodes; Make sure there are no siblings to process before moving on to the parent node. This means that completeWork execution is strictly bottom-up, and the completeWork of the child node is always executed before the parent node.

2) design and implementation of side effectList

Both beginWork and completeWork are applied to nodes in the workInProgress tree. We say that the Render phase is a recursive process, and the object of the “recursion” is the workInProgress tree (see highlighted section on the right side of the image below) :

So what’s the purpose of recursion? In other words, what is the goal of the Render phase?

(1) The objective of the Render phase is to identify the updates that need to be processed in the interface.

In practice, not all nodes will have updates that need to be processed. For example, in the mount phase, after the whole workInProgress is recursed, React will find that only one mount operation is required on the App node. In the renewal phase, this phenomenon is more obvious.

The main difference between the update phase and the mount phase is that the current tree in the update phase is not empty. For example, the situation could look like the following:

If only the P node is affected in an operation, then the renderer should only care about updates at the P node. The question then arises: how can the renderer quickly and well locate the nodes that really need to be updated?

In the Render phase, we went through the hard recursive process of figuring out “there’s an update here at the P node”. According to the React design concept, after the Render phase is over, the “find different” thing will come to an end. Commit is only responsible for implementing updates, not finding them, which means we have to find a way for the Commit phase to “ride the wave” and get direct access to the render phase’s work. And that’s where the value of the side effectList comes in.

A side effectList can be understood as a collection of “work products” of the render phase: Each Fiber node maintains its own effectList in the form of a linked list of data structures, each element of which is a Fiber node. These Fiber nodes need to satisfy two commonalities:

  • Both are descendants of the current Fiber node

  • All side effects to be dealt with

Yes, the Fiber node’s effectList records not its own updates, but its descendants. With this conclusion in mind, go back to “Step 2” in the beginning of the tasting section completeUnitOfWork:

Inserts the effectList of the current node into the effectList of its parent node.

As analyzed earlier, “completeWork executes bottom-up,” meaning that the completeWork of the child node is always executed before the parent node. Imagine inserting the effectList of the current node into the effectList of its parent node each time you process a node. When the completeWork of all nodes is complete, is it possible to get the ultimate effectList of all effect fibers in the current Fiber tree from the ultimate parent, rootFiber?

String all the Fiber nodes that need to be updated into a single linked list so that they can be updated later on, a process called “collecting side effects.”

(2) Here, the mount process is taken as an example to analyze how this process (side effect collection process) is implemented.

The first thing we need to know is that the effectList is maintained by firstEffect and lastEffect in the Fiber node, as shown below:

Where firstEffect represents the first node of the effectList and lastEffect records the last node.

For the mount process, the only thing that needs to be done is to mount the App component to the interface, so the effectList of the descendants of the App nodes does not actually exist. The effectList is not empty only in the parent of the App (rootFiber).

What about the creation logic of effectLists? It’s as simple as assigning a reference to firstEffect and a reference to lastEffect. The following logic is extracted from the source code of completeUnitOfWork (explained in the comments) :

// If the side effect type is greater than "PerformedWork", If (flags > PerformedWork) {// returnFiber is the parent of the current node if (returnFiber. LastEffect! = = null) {/ / if the parent node effectList isn't empty, will be appended to the current node to returnFiber. At the end of effectList lastEffect. NextEffect = completedWork; } else {// If the parent's effectList is empty, the current node is firstEffect returnFiber of the effectList. FirstEffect = completedWork; } // Move the lastEffect pointer to the effectList by one returnFiber. LastEffect = completedWork; }Copy the code

Flags in the code have been emphasized over and over again. The old name for flags is effectTag, which identifies the type of side effect; The “completedWork” variable, in its current context, stores the node where “completeWork logic is being executed”; As for “PerformedWork”, which is a constant with a value of 1, React states that if the value of flags (effectTag) is less than or equal to 1, it does not have to commit to the COMMIT phase. Therefore completeUnitOfWork will only collect effect fibers whose flags are greater than PerformedWork.

Combined with this information, read the source code snippet again, and believe that your understanding process will be very smooth. Create an effectList using an App node as an example:

  • App FiberNode has a flags attribute of 3, which is larger than PerformedWork, so it enters the creation logic of effectList.

  • When creating an effectList, it is not created for the current Fiber node, but for its parent node. The parent node of App node is rootFiber, and the effectList of rootFiber is empty.

  • Both firstEffect and lastEffect Pointers of rootFiber point to the App node, which becomes the only FiberNode in the effectList, as shown in the figure below.

At this point, you are familiar with the process of creating effectLists.

Now, even though you may not be able to digest some of the source details as quickly, don’t let those details stop you from connecting the entire render link. The conclusion that the effectList on the rootFiber is the update clue for the commit phase is enough to link the render phase with the commit phase.

3. Brief analysis of workflow in COMMIT phase

In the whole reactdom.render render link, the Render stage is the core embodiment of Fiber architecture and also the focus of the explanation. For the Render phase, be “familiar”; For the COMMIT phase, get to know. So here’s a quick overview of the commit phase:

Commit is called in performSyncWorkOnRoot, as shown in the following figure:

The input parameter root is not rootFiber, but fiberRoot (FiberRootNode) instance. FiberRoot’s current node points to rootFiber, so fetching the effectList should not be too difficult for the subsequent commit process.

(2) In terms of process, commit is divided into three stages: before mutation, mutation and layout.

  • Before mutation phase.

The DOM node is not rendered to the interface at this stage, getSnapshotBeforeUpdate is triggered, and the useEffect hook related scheduling logic is also handled.

  • Mutation, this stage is responsible for rendering DOM nodes.

During rendering, the effectList is iterated over, performing different DOM operations depending on flags (EffectTags).

  • Layout, which handles the finishing logic after the DOM is rendered.

Such as call componentDidMount/componentDidUpdate, call useLayoutEffect hook function callback, etc. In addition to this, it points the fiberRoot current pointer to the workInProgress Fiber tree.

For details about the implementation of the COMMIT phase, you can refer to the COMMIT source code, which is not discussed here. If you can only remember one thing about commit, you should remember that commit is an absolutely synchronous process. The Render phase can be synchronous or asynchronous, but the COMMIT must be synchronous.

4, summarize

This tutorial completes an analysis of the Reactdom.render call stack. On the surface, the first rendering link is dissected. In fact, the mount and update link in synchronous mode (very similar to the call stack of the mount link) are connected in series.

While we have not yet formally entered into the update link, including the asynchronous update mode, we have the basics to understand: Concurrent mode is the same as Legacy mode in terms of data structure design, core API calls, etc. This means that all of the knowledge discussed in these three lectures can be reused in future studies.

Next, we’ll learn about the update process and uncover the mysteries of Concurrent mode and Scheduler. At the same time, the question “why do we need two trees” left over from the last lecture will be answered in the next lecture.

5, the appendix

To study the source