preface
Recently began to deeply study the principle of the React, behind will be out of a series of articles about the React principle, basic it is I learn other elder React source code analysis, and follow them to read the source code of some thinking and record, most of the original content, but I will use my own way to summarize principle and related processes, and complement, Take it as a summary of your own learning.
This series focuses on the underlying source code implementation and is not recommended if you are new to React.
React renders the first time.
For the first rendering, the React code becomes the flow of the DOM
There are two main steps. The first is to turn the JSX code into a virtual DOM using the React. CreateElement method, and the second is to turn the virtual DOM into a real DOM using the reactdom.render method.
React.createElement
The React.createElement method may not be known to many newcomers because it is rarely used directly in normal business logic. In fact, the React website already has a description.
JSX will compile to react.createElement (), and react.createElement () will return a JS object called “React Element”.
We are inbabelYou can put one on the websiteJSX
Have a try.
After compiling with Babel, JSX becomes a nested react.createElement. JSX is essentially a syntactic sugar for JavaScript calls called React.createElement. With the existence of JSX syntactic sugar, we can use htML-like tag syntax we are most familiar with to create virtual DOM, which not only reduces learning cost, but also improves r&d efficiency and experience.
Next, take a look at the createElement source code
export function createElement(type, config, children) {
// The propName variable is used to store element attributes that need to be used later
let propName;
// props is the set of key-value pairs used to store element attributes
const props = {};
// Key, ref, self, and source are all React elements
let key = null;
let ref = null;
let self = null;
let source = null;
The config object stores the attributes of the element
if(config ! =null) {
// The first thing you do is assign the ref, key, self, and source attributes in order
if (hasValidRef(config)) {
ref = config.ref;
}
// String the key here
if (hasValidKey(config)) {
key = ' ' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// Move all the properties in config to props one by one
for (propName in config) {
if (
// Filter out properties that can be pushed into the props objecthasOwnProperty.call(config, propName) && ! RESERVED_PROPS.hasOwnProperty(propName) ) { props[propName] = config[propName]; }}}// childrenLength is the number of children of the current element; the 2 subtracted is the length of the type and config parameters
const childrenLength = arguments.length - 2;
// If type and config are omitted, there is only one argument left, which generally means that the text node is present
if (childrenLength === 1) {
// assign this parameter directly to props. Children
props.children = children;
// Handle nested child elements
} else if (childrenLength > 1) {
// Declare an array of children
const childArray = Array(childrenLength);
// Push the child element into the array
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
// Finally, assign this array to props. Children
props.children = childArray;
}
/ / defaultProps processing
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) { props[propName] = defaultProps[propName]; }}}// Finally returns a call to ReactElement execution method, passing in the arguments just processed
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
Copy the code
So let me summarize some of the things that this function does
- 1. Secondary processing
key
.ref
.self
.source
Four values (willkey
Stringing, willconfig
In theref
Assigned toref
.self
andsource
I don’t know its function, so I can ignore it.The JSX method in version 17 removes these two parameters directly) - 2. Traversal
config
, filter can be assigned toprops
In the properties of the - 3. Extract the child element and assign the value to
props.children
(If there is only one child element, it is directly assigned, if there is more than one child element, it is stored as an array) - 4. The format
defaultProps
(If no relevant is passed inprops
.props
Take the default Settings.) - 5. Return one
ReactElement
Method and pass in the arguments you just processed
CreateElement is essentially a data handler that formats the content retrieved from JSX and passes it into the ReactElement method.
Note: In React 17, createElement will be replaced with the JSX (source address) method.
import React from 'react'; // In version 17, it is possible not to introduce this sentence
function App() {
return <h1>Hello World</h1>;
}
// createElement
function App() {
return React.createElement('h1'.null.'Hello world');
}
// JSX in version 17
import {jsx as _jsx} from 'react/jsx-runtime'; // imported by the compiler
function App() {
// The child element will be compiled directly into the children attribute of the Config object. JSX will no longer accept individual child element inputs
return _jsx('h1', { children: 'Hello world' });
}
Copy the code
ReactElement
Let’s look at what the ReactElement method does, the source address.
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// REACT_ELEMENT_TYPE is a constant that identifies the object as a ReactElement
$$typeof: REACT_ELEMENT_TYPE,
// Built-in attribute assignment
type: type,
key: key,
ref: ref,
props: props,
// Record the component that created the element
_owner: owner,
};
//
if (__DEV__) {
// Omit unnecessary code
}
return element;
};
Copy the code
The ReactElement method is also very simple. It simply creates an object from the parameters passed in and returns it. The ReactElement object instance is what the createElement method finally returns, and is a node in the React virtual DOM. The virtual DOM is essentially an object that stores a number of properties used to describe the DOM.
Note that since each node is called by the createElement method, it should return a tree of the virtual DOM.
const App = (
<div className="App">
<h2 className="title">title</h2>
<p className="text">text</p>
</div>
);
console.log(App);
Copy the code
As shown in the figure above, all nodes are compiled into ReactElement object instances (virtual DOM).
ReactDOM.render
We have the virtual DOM, but ultimately we want to render the content on the page, so we also need to render the virtual DOM into the real DOM using the reactdom.render method. This is a lot of content, and I will discuss it one function at a time.
As for why to use the virtual DOM, what are the advantages of the virtual DOM, these contents are not the scope of this article, there are many online related articles, you can go to understand.
Three kinds ofReact
Start the way
React versions 16 and 17 have always had three boot options
legacy
Mode,ReactDOM.render(<App />, rootNode)
. In the current common mode, the rendering process is synchronousblocking
Mode,ReactDOM.createBlockingRoot(rootNode).render(<App />)
. The transition mode is rarely usedconcurrent
Mode,ReactDOM.createRoot(rootNode).render(<App />)
. The asynchronous rendering mode is also available with some new features that are currently being experimented with.
The official documentation
We are parsing the reactdom.render process, so we are actually analyzing a synchronous process. In terms of Concurrent’s asynchronous rendering process, time sharding and prioritization, these are actually modifications based on synchronous rendering, which I will cover in a later article in this series.
It’s a synchronous process, but in React 16, the entire render link was reconfigured to a Fiber structure. Fiber architecture in React is not strictly equal to asynchronous rendering. It is a design compatible with both synchronous and asynchronous rendering.
For the first timerender
The three stages of
The reactdom.render method corresponds to a very deep call stack and involves many function methods, but we can just look at some key logic to get a sense of the flow.
The first render can be roughly divided into three stages
- Initialization phase, complete
Fiber
Creation of a base entity in a tree. From the callReactDOM.render
Began to,scheduleUpdateOnFiber
The method callperformSyncWorkOnRoot
The end. - Render phase, build and refine
Fiber
The tree. fromperformSyncWorkOnRoot
Method start, tocommitRoot
Method ends. - Commit phase, traversal
Fiber
The trees,Fiber
Node mapping isDOM
Node and render to the page. fromcommitRoot
Method starts and ends rendering.
It doesn’t matter if you don’t understand the above methods now, we will take you step by step to understand. You just need to get a sense of what each of the three stages does.
Initialization phase
Now we begin the initialization phase, which, as mentioned above, is all about completing the creation of the basic entities in the Fiber tree. But we need to know what are the basic entities? What are the? We look for the answer in the source code.
legacyRenderSubtreeIntoContainer
We only look at the key logic, now let’s look at ReactDOM. Render legacyRenderSubtreeIntoContainer method (source address) in the call.
// call in reactdom.render
return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
/ / legacyRenderSubtreeIntoContainer source code
function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
// Container corresponds to the real DOM object we passed in
var root = container._reactRootContainer;
// Initialize the fiberRoot object
var fiberRoot;
// The DOM object itself does not have the _reactRootContainer property, so root is empty
if(! root) {// If root is empty, initialize _reactRootContainer and assign its value to root
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
/ / legacyCreateRootFromDOMContainer create objects can have a _internalRoot attribute, its assigned to fiberRoot
fiberRoot = root._internalRoot;
// This is the callback function in the reactdom. render argument
if (typeof callback === 'function') {
var originalCallback = callback;
callback = function () {
var instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
} // Initial mount should not be batched.
// Enter the unbatchedUpdates method
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
// The else logic deals with the non-first render (i.e. update) case, and the logic is basically the same as upstairs, except that initialization is skipped
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
var _originalCallback = callback;
callback = function () {
var instance = getPublicRootInstance(fiberRoot);
_originalCallback.call(instance);
};
} // Update
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
Copy the code
This function basically does the following steps
- 1. Call
legacyCreateRootFromDOMContainer
Method createscontainer._reactRootContainer
And assigned to itroot
- 2.
root
the_internalRoot
Attribute assigned tofiberRoot
- 3.
fiberRoot
Passed in with some other parametersupdateContainer
methods - The 4.
updateContainer
Is passed in as an argumentunbatchedUpdates
methods
Here,fiberRoot
The essence of aFiberRootNode
Object whose associated object is trueDOM
There is one in this objectcurrent
objectAs shown above, this onecurrent
The object is aFiberNode
Instance, in fact, it’s oneFiber
Node, and she is still currentFiber
The head node of a tree.fiberRoot
And the one below itcurrent
Object and these two nodes will be the whole subsequent treeFiber
The starting point for tree construction.
unbatchedUpdates
Let’s look at the unbatchedUpdates method (source address).
function unbatchedUpdates(fn, a) {
// This is the context
var prevExecutionContext = executionContext;
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
// The important point here is to call the incoming callback function fn directly, corresponding to the updateContainer method in the current link
return fn(a);
} finally {
// Finally is the processing of the callback queue
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batchresetRenderTimer(); flushSyncCallbackQueue(); }}}Copy the code
This method is relatively simple and simply calls the incoming callback function fn. And fn, is in legacyRenderSubtreeIntoContainer incoming
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});
Copy the code
So let’s look at the updateContainer method again
updateContainer
Looking at the source code (source address) first, I will remove a lot of extraneous logic.
function updateContainer(element, container, parentComponent, callback) {
// This current is the head node of the current 'Fiber' tree
const current = container.current;
// This is an event-related entry, so don't worry about it here
var eventTime = requestEventTime();
// This is a key entry. Lane represents the priority
var lane = requestUpdateLane(current);
// Create an update object with lane (priority) information. An update object means an update
var update = createUpdate(eventTime, lane);
// Update payload corresponds to a React element
update.payload = {
element: element
};
// Handle the callback, which is actually the callback passed in when we call reactdom.render
callback = callback === undefined ? null : callback;
if(callback ! = =null) {{if (typeofcallback ! = ='function') {
error('render(...) : Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback);
}
}
update.callback = callback;
}
// Add the update to the team
enqueueUpdate(current, update);
/ / scheduling fiberRoot
scheduleUpdateOnFiber(current, lane, eventTime);
// Returns the priority of the current node (fiberRoot)
return lane;
}
Copy the code
The logic of this method is a bit complicated, but it can be broken down into three main points
- 1. Request current
Fiber
The node’slane
(Priority) - 2. The combination of
lane
(Priority) to create the currentFiber
The node’supdate
Object and enqueue it - 3. Schedule the current node (
rootFiber
) to update
However, since the first rendering link explained in this article is synchronous, the priority is not significant, so we can directly look at the method of scheduling nodes scheduleUpdateOnFiber.
scheduleUpdateOnFiber
This method is a bit long, so I just list the key logic (source address).
// If the rendering is synchronous, this condition will be entered. If the mode is asynchronously rendered, it goes into its else logic
// React uses fibre. mode to differentiate between different rendering modes
if (lane === SyncLane) {
if (
// Check whether the unbatchedUpdates method is currently running(executionContext & LegacyUnbatchedContext) ! == NoContext &&// Check whether the current is render
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
schedulePendingInteractions(root, lane);
// The key step to focus on starts with this method. Start render phase
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.resetRenderTimer(); flushSyncCallbackQueue(); }}}Copy the code
In the previous steps, React created the basic entities in the Fiber tree, which are the fiberRoot nodes and the Current object underneath. In this method, we only need to focus on the performSyncWorkOnRoot method, from which we will proceed to the Render phase.
Render phase
The render phase builds and refines the Fiber tree, essentially iterating through fiberRoot and its current object as the top nodes to build the Fiber tree of their child elements.
Let’s start with the performSyncWorkOnRoot method.
performSyncWorkOnRoot
PerformSyncWorkOnRoot source code address
Here we focus on two pieces of logic
exitStatus = renderRootSync(root, lanes); . commitRoot(root);Copy the code
The renderRootSync method marks the start of the Render phase, and the commitRoot below marks the start of the Commit phase. We entered the Render phase first, so let’s look at the flow in renderRootSync first.
RenderRootSync source address
There are two pieces of logic to look at in this method
prepareFreshStack(root, lanes); . workLoopSync();Copy the code
Let’s go through the prepareFreshStack process and wait for it to complete before entering the workLoopSync traversal process.
PrepareFreshStack source address
The purpose of prepareFreshStack is to reset a new stack environment, and we only need to focus on one logic
workInProgress = createWorkInProgress(root.current, null);
Copy the code
CreateWorkInProgress is an important method, so let’s look at it in detail.
createWorkInProgress
The simplified source code is as follows, the source address
// Current passes the rootFiber object in the existing tree structure
function createWorkInProgress(current, pendingProps) {
var workInProgress = current.alternate;
// reactdom.render triggers the first screen render to enter this logic
if (workInProgress === null) {
// This is the first point you need to focus on. WorkInProgress is the return value of createFiber
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
// This is the second point you need to pay attention to, workInProgress alternate will point to current
workInProgress.alternate = current;
// This is the third point you need to pay attention to, current alternate will in turn point to workInProgress
current.alternate = workInProgress;
} else {
// Do not worry about the else logic here
}
// Omit much of the property handling logic for workInProgress objects below
// Return to the workInProgress node
return workInProgress;
}
Copy the code
A little bit of a caveat here, inputcurrent
That’s the one beforefiberRoot
The object ofcurrent
Object.
To summarize what the createWorkInProgress method does
- 1. Call
createFiber
.workInProgress
iscreateFiber
Method return value - The 2.
workInProgress
thealternate
Will point tocurrent
- The 3.
current
thealternate
Point the other wayworkInProgress
- 4. Finally return one
workInProgress
node
The createFiber method here, as its name implies, is used to create a Fiber node. The input parameters are values of current, so the workInProgress node is actually a copy of the current node. The structure of the tree should look like this:
With the workInProgress tree vertex created, now run the second key logic, workLoopSync, from the renderRootSync method.
workLoopSync
This method is very simple, just a traversal function
function workLoopSync() {
// If workInProgress is not empty
while(workInProgress ! = =null) {
// Execute the performUnitOfWork method on itperformUnitOfWork(workInProgress); }}Copy the code
The methods listed below are iterated over and over in workLoopSync, so before I parse the performUnitOfWork method and its submethods, I will summarize the entire traversal process and analyze the methods once I have a general understanding.
All workLoopSync does is iterate through the while loop to determine whether workInProgress is empty and execute the performUnitOfWork function on it if it isn’t. The performUnitOfWork function triggers a call to the beginWork, creating a new Fiber node. If the Fiber node created by beginWork is not empty, formUniofWork uses the new Fiber node to update the value of workInProgress in preparation for the next loop.
When workInProgress is empty, it means that the entire Fiber tree has been built.
During this process, each new Fiber node created is mounted as a descendant of the previous workInProgress tree. Let’s look at it step by step.
performUnitOfWork
The source address
next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
Copy the code
There are actually two processes in performUnitOfWork, one is the beginWork process (creating a new Fiber node), and another is the completeWork process (when the beginWork traverses the leaf node of the current branch, next === null, Run the completeWork process) to handle the Fiber node to DOM node mapping logic.
Let’s start with the beginWork process
beginWork
The beginWork code is more than 400 lines, which is way too much, just to take some key logic. The source address
function beginWork(current, workInProgress, renderLanes) {...// If the current node is not empty, an identification is added to see if there is any update logic to process
if(current ! = =null) {
// Get old and new props
var oldProps = current.memoizedProps;
var newProps = workInProgress.pendingProps;
// If the props are updated or the context changes, "accept the update" is considered necessary.
if(oldProps ! == newProps || hasContextChanged() || ( workInProgress.type ! == current.type )) {// Put an update mark
didReceiveUpdate = true;
} else if (xxx) {
// Do not need to update the case A
return A
} else {
if(Case B) {didReceiveUpdate =true;
} else {
// Other cases where no updates are required, here our first rendering will execute the logic to this line
didReceiveUpdate = false; }}}else {
didReceiveUpdate = false; }...// This batch of switch is the core logic of beginWork, the original code is quite large
switch (workInProgress.tag) {
......
// Omit a lot of logic in the form of "case: XXX"
// The root node will enter this logic
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes)
// The node corresponding to the DOM tag will enter this logic
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes)
// The text node will enter this logic
case HostText:
return updateHostText(current, workInProgress)
......
// Omit a lot of logic in the form of "case: XXX"}}Copy the code
The core logic of beginWork is to call different node creation functions based on the different tag attributes of fiber nodes (nodes under the workInProgress tree) (which represent the type of tag that fiber currently belongs to).
These node-creation functions all end up generating children of the current node by calling the reconcileChildren method.
ReconcileChildren (beginWork Process)
This method is also relatively simple
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
// Check whether current is null
if (current === null) {
// If current is null, the logic of mountChildFibers is entered
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
// If current is not null, it enters the logic of reconcileChildFibersworkInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes); }}Copy the code
In the above two methods, we can also find where to assign values
var reconcileChildFibers = ChildReconciler(true);
var mountChildFibers = ChildReconciler(false);
Copy the code
Both methods are created using the ChildReconciler method, with different input parameters
ChildReconciler (beginWork process)
ChildReconciler code volume is also very large, code will not put, source address.
This method contains many functions for creating, adding, deleting, and modifying Fiber nodes for other functions to call. The return value is a function called reconcileChildFibers, which is a logical distributor that performs different Fiber node operations, depending on the reconcileChildFibers, and ultimately returns different target Fiber nodes.
The reconcileChildFibers and mountChildFibers differ in how they are dealt with, according to the reconcileChildFibers and mountChildFibers. ShouldTrackSideEffects should be true to add a flags attribute to the newly created Fiber node (before version 17, this attribute was effectTag) and assign a constant.
In the case of the root node, a Placement constant is assigned, which is a binary constant that tells the renderer, when rendering the real DOM, that the new DOM node is needed to process the Fiber node. There are many other constants of this type, source code addresses.
Here is a demo that will be used for the rest of the compilation.
function App() {
return (
<div className="App">
<div className="container">
<h1>I am heading</h1>
<p>I'm the first paragraph</p>
<p>I'm the second paragraph</p>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Copy the code
Going back to the render link that we just had, because this cycle is the first to deal with the top nodes of the Current tree and the workInProgress tree, the Current exists and goes into the reconcileChildFibers method, which allows side effects to be tracked. Since the current workInProgress is the top node, it does not have an exact ReactElement mapping to it, so it will be the parent node of the root component in JSX, that is, the parent node of the App component. FiberNode is then created based on the App component’s ReactElement(jSX-compiled virtual DOM) object information, marked with Placement side effects, and returned to WorkinProgress.Child.
So it will beJSX
The root of the componentFiber
With the one created earlierFiber
Tree vertices are associated, as shown below.
The first loop is completed, and the workInProgress returned by the beginWork is not null because the App has child elements (workInProgress is actually the fiber node after compiling these JSX nodes), and the workLoopSync loop will continue. The final tree structure is as follows
Let’s take a look at these labelsfiber
Node object.
The image above shows the App node, the two div child nodes, and the p tag node. As you can see, each non-text ReactElement has its corresponding Fiber node.
These nodes are all connected with each other. They establish relationships through the three attributes child, return and Sibling, among which child and return record the parent-child node relationship. Sibling refers to the first sibling of the current node.
See the following figure for details:
This is the final form of the workInProgress Fiber tree. As you can see, while the product in question is still conventionally referred to as a Fiber tree, the nature of its data structure has changed from a tree to a linked list.
Let’s look at another completeWork process.
CompleteUnitOfWork (completeWork process)
As mentioned above, in performUnitOfWork, after the beginWork process traverses to the leaf node, next will become null. When the next beginWork process ends, the corresponding completeWork process enters.
Rereference the code in the performUnitOfWork method used above
next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
Copy the code
CompleteUnitOfWork is a method that iterates through the loop, and it iterates through the following things in the loop
- 1. Call
completeWork
methods - 2. Add the current node’s side effect chain (
EffectList
) inserted into its parent’s side effect chain (EffectList
) - 3. Start from the current node and iterate through its siblings and parent nodes. When traversed to the sibling node, will
return
Drop the current call, triggering the sibling node correspondingperformUnitOfWork
Logic; When the parent node is traversed, it goes directly to the next loop, which repeats the logic of 1 and 2
Let’s start with the completeWork method.
CompleteWork (completeWork process)
CompleteWork source code address
CompleteWork is also a big function, and we only pull out the key logic
function completeWork(current, workInProgress, renderLanes) {
// Get the properties of the Fiber node and store them in newProps
var newProps = workInProgress.pendingProps;
// Depending on the tag attribute of the workInProgress node, decide which logic to enter
switch (workInProgress.tag) {
......
// The h1 node type belongs to HostComponent, so this is the logic here
case HostComponent:
{
popHostContext(workInProgress);
var rootContainerInstance = getRootHostContainer();
var type = workInProgress.type;
// Check whether the current node exists. The current node does not exist because it is currently mounted
if(current ! = =null&& workInProgress.stateNode ! =null) {
updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// Return for exceptions.// Now we are ready to create the DOM node
var currentHostContext = getHostContext();
// _wasphase is a server-side rendering - dependent value, which is not a concern here
var _wasHydrated = popHydrationState(workInProgress);
// Determine if it is server render
if (_wasHydrated) {
......
} else {
// This step is crucial. CreateInstance creates a DOM node
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
// appendAllChildren attempts to mount the DOM node created in the previous step into the DOM tree
appendAllChildren(instance, workInProgress, false.false);
// stateNode is used to store the DOM node corresponding to the current Fiber node
workInProgress.stateNode = instance;
FinalizeInitialChildren is used to set attributes for DOM nodes
if(finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) { markUpdate(workInProgress); }}... }return null;
}
case HostText:
{
......
}
case SuspenseComponent:
{
......
}
case HostPortal:
......
return null;
case ContextProvider:
......
return null; . }}Copy the code
So the first thing we need to know is going into thiscompleteWork
What are the parameters of phi that we know only whenbeginWork
You don’t enter until you’ve traversed the first leaf nodecompleteWork
Methods. So, when you first run it, the arguments are actually in the demoh1
Tag corresponding tofiber
Node object. This is alsocompleteWork
One of the features is that it runs strictly from the bottom up.
Then let’s look at some of the functional points of the completeWork method
- 1.
completeWork
The core logic of the book is a huge piece ofswitch
Statement, in this paragraphswitch
Statement,completeWork
Based on theworkInProgress
The node’stag
Different attributes, into differentDOM
Node creation and processing logic. - In 2.
Demo
In the sample,h1
The node’stag
The type of the property should beHostComponent
That is, nativeDOM
Element type. - 3.
completeWork
In thecurrent
,workInProgress
That’s what I said beforecurrent
Trees andworkInProgress
Nodes on the tree.
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 top node and nothing else. Therefore, the current node corresponding to the h1 workInProgress node is null.
With that premise in mind, let’s look at the completeWork method again, and we can conclude that
CompleteWork handles the mapping logic from Fiber nodes to DOM nodes. Through three steps
- 1. Create
DOM
Node (CreateInstance
) - 2.
DOM
Nodes are inserted into the DOM tree (AppendAllChildren
), assign toworkInProgress
The node’sstateNode
Properties (In addition, when the current node runs AppendAllChildren, it looks up its descendant sub-fiber nodes one by one and attaches the corresponding DOM node to the DOM node corresponding to its parent Fiber node. Therefore, the stateNode attribute in the highest level node, It’s a whole DOM tree) - 3. To
DOM
Node Settings properties (FinalizeInitialChildren
)
Step 2,3 (completeUnitOfWork process)
Let’s look at the code implementation of step 3
Starting with the current node, loops through its siblings and their parents. When traversing the sibling node, return the current call and trigger the performUnitOfWork logic corresponding to the sibling node. When the parent node is traversed, it goes directly to the next loop, which repeats the logic of 1 and 2
do{...The logic of steps 1 and 2 is omitted here
// Get the sibling of the current node
var siblingFiber = completedWork.sibling;
// If sibling nodes exist
if(siblingFiber ! = =null) {
// Assign workInProgress to a sibling of the current node
workInProgress = siblingFiber;
// Return the completeUnitOfWork logic in progress
return;
}
// If the sibling does not exist, completeWork is assigned returnFiber, which is the parent of the current node
completedWork = returnFiber;
// This step is complementary to the previous step, and the context requires that the workInProgress be consistent with completedWork
workInProgress = completedWork;
} while(completedWork ! = =null);
Copy the code
The function is relatively simple, according to the demo, because the beginWork process is a depth-first traversal. When traversing the H1 tag, the traversal is interrupted and the completedWork process starts to be executed. H1 sibling p tag, actually the beginWork process has not run, so you need to call performUnitOfWork logic again.
Let’s talk about step two
Inserts the current node’s EffectList into its parent node’s EffectList.
The goal of this step is to identify the updates in the interface that need to be handled. Because in the actual operation, not all nodes will have updates that need to be processed. For example, in the mount phase, after the whole workInProgress tree is recursed, React will find that only one App node needs to be mounted. In the renewal phase, this phenomenon is more obvious.
How can the renderer quickly and well locate the nodes that really need to be updated? That’s what a side effectList does.
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:
- Is the current
Fiber
Descendants of a node (not its own updates, but its descendants that need to be updated) - All side effects to be dealt with
thiseffectList
List inFiber
The node is throughfirstEffect
andlastEffect
To maintain.firstEffect
saideffectList
The first node of, andlastEffect
The last node is recorded.
Because the completeWork is executed bottom-up, on the top node you get an effect Fiber that stores all of the current Fiber tree.
In the demo, only the top node has a side effect chain (the Fiber node of the App component), and no side effect chain exists for all child nodes within the App component. When rendering or updating for the first time, the renderer will only deal with the App Fiber node in the side effect chain (App, as the smallest updated component, already contains the DOM node of the inner child element). Of course, if there are other components referenced in the App, the fiber of the App component will also contain the side effect chain of that component.
The commit phase
Commit is called in performSyncWorkOnRoot, which is an absolutely synchronous process.
commitRoot(root);
Copy the code
The source address
In terms of process, COMMI is divided into three stages: before mutation, mutation and layout.
-
The before mutation stage, in which the DOM node has not been rendered to the interface, triggers getSnapshotBeforeUpdate and also handles the useEffect hook scheduling logic.
-
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.
Thank you
If this article helped you, please give it a thumbs up. Thanks!