preface
In this article we’ll see how React handles state updates. And how to build an Effects List. We’ll go into detail about what happens in the Render phase and the Commit phase.
We’ll see how React works in the completeWork function:
- Update the state property.
- Call the Render method and compare the child nodes.
- Update props for the React element.
And in the commitRoot function React:
- Update the textContent attribute of the element.
- Call the componentDidUpdate lifecycle method.
But before we do that, let’s look at how React works when we call setState. Let’s use an example from the previous article for the convenience of this article, a simple counter component:
class ClickCounter extends React.Component {
constructor(props) {
super(props);
this.state = {count: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState((state) = > {
return {count: state.count + 1};
});
}
componentDidUpdate() {}
render() {
return [
<button key="1" onClick={this.handleClick}>Update counter</button>.<span key="2">{this.state.count}</span>]}}Copy the code
Schedule updated
When we click the button, the Click event is triggered and React executes our callback. In our application, it will update our state.
class ClickCounter extends React.Component {...handleClick() {
this.setState((state) = > {
return {count: state.count + 1}; }); }}Copy the code
Each React component has an associated updater. The Updater acts as a bridge between components and React Core. This allows setState to be implemented in different ways in ReactDOM, React Native, server-side rendering, and test cases.
Here, we will focus on the implementation of the updater in the ReactDOM. It uses Fiber Reconciler. For the ClickCounter component, it is a classComponentUpdater. It is responsible for retrieving Fiber instances, queuing updates, and scheduling work.
At update time, the updater adds an update queue to the Fiber node. In our example, the ClickCounter component corresponds to the Fiber node structure as follows:
{
stateNode: new ClickCounter, // Keep references to class component instances
type: ClickCounter, // The type attribute points to the constructor
updateQueue: { // State update and callback, DOM update queue.
baseState: {count: 0}
firstUpdate: {
next: {
payload: (state) = > { return {count: state.count + 1}}}},... },... }Copy the code
As you can see updateQueue. FirstUpdate. Next, the content of the payload is our passing in the setState callback. This represents the first update that needs to be processed in the Render phase.
Handle updates to ClickCounter’s Fiber node
The role of the nextUnitOfWork global variable was introduced in the previous article.
NextUnitOfWork maintains a reference to a Fiber node in the workInProgress Tree that has work to process. NextUnitOfWork will refer to the next Fiber node reference or null. You can use the nextUnitOfWork variable to determine if there are any Fiber nodes that have completed their work.
We assume and call setState, React adds the setState callback to the updateQueue field in the Fiber node of ClickCounter and schedules the work. React entered the render phase. It starts with the HostRoot node and iterates the Fiber tree using the renderRoot function. It skips Fiber nodes that have already been processed until it finds nodes whose work is incomplete. At this point, only one Fiber node has unfinished work, the ClickCounter Fiber node.
The first node in the Fiber tree is a special type of node called HostRoot. It is created internally and is the parent of the topmost component
All the “work” will be done on the Fiber node backup. Backup is stored in the alternate field. If a backup node has not been created, React creates a backup using the createWorkInProgress function before processing the update. Assume nextUnitOfWork has a reference to the Fiber node of ClickCounter.
beginWork
First, Fiber enters the beginWork function.
The beginWork function is performed by each Fiber node. So if you need to debug the render phase of the source code, this is a good place to put breakpoints. I do that all the time.
Inside the beginWork function is a large switch statement that determines the type of the Fiber node based on its tag attribute. The corresponding function is then executed to perform the work.
Our node is the Fiber node of the CountClicks component, so it goes into the ClassComponent branch statement
function beginWork(current$$1, workInProgress, ...) {...switch (workInProgress.tag) {
...
caseFunctionalComponent: {... }case ClassComponent:
{
...
returnupdateClassComponent(current$$1, workInProgress, ...) ; }caseHostComponent: {... }case. }Copy the code
Then we go to the updateClassComponent function
In the updateClassComponent function, determine whether the component is either rendering for the first time, resuming work (the Render phase can be interrupted), or updating. React either creates an instance and mounts the component, or simply updates it.
function updateClassComponent(current, workInProgress, Component, ...) {...const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {...If the instance is null, we need to construct the instanceconstructClassInstance(workInProgress, Component, ...) ; mountClassInstance(workInProgress, Component, ...) ; shouldUpdate =true;
} else if (current === null) {
// After a restart, we have an instance that we can reuse.shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...) ; }else {
// Just updateshouldUpdate = updateClassInstance(current, workInProgress, ...) ; }returnfinishClassComponent(current, workInProgress, Component, shouldUpdate, ...) ; }Copy the code
Handle updates to CountClicks Fiber
Now that we have an instance of the ClickCounter component with the beginWork and updateClassComponent functions, we go to updateClassInstance. The updateClassInstance function is where React does most of the work for the class components. Here are the most important operations performed in the function (in order of execution):
- Executive function UNSAFE_componentWillReceiveProps life cycle
- Execute the updateQueue for updateQueue in the Fiber node to generate the new state
- With the new state, execute getDerivedStateFromProps and get the result
- ShouldComponentUpdate to determine if the component needs to be updated. If false, skip the entire Render processing, including this component and its children. If true, continue updating.
- Execute the UNSAFE_componentWillUpdate life cycle function
- Add effect to trigger the componentDidUpdate lifecycle function
- Update the state and props on the component instance
Although the effect for componentDidUpdate is added in the Render phase, the method will be executed in the next commit phase
The state and props should be updated before the Render method of the component instance is called, because the output of the Render method usually depends on the state and props, and if we don’t, it will return the same output every time.
// The simplified code
function updateClassInstance(current, workInProgress, ctor, newProps, ...) {
// An instance of the component
const instance = workInProgress.stateNode;
// Props before
const oldProps = workInProgress.memoizedProps;
instance.props = oldProps;
if(oldProps ! == newProps) {/ / if there are differences between the current props and props before, execute UNSAFE_componentWillReceivePropscallComponentWillReceiveProps(workInProgress, instance, newProps, ...) ; }// Update the queue
let updateQueue = workInProgress.updateQueue;
if(updateQueue ! = =null) {
// Execute the update queue to get the new statusprocessUpdateQueue(workInProgress, updateQueue, ...) ;// Get the latest state
newState = workInProgress.memoizedState;
}
// With the latest state, call getDerivedStateFromPropsapplyDerivedStateFromProps(workInProgress, ...) ;// Get the latest state (getDerivedStateFromProps may update state)
newState = workInProgress.memoizedState;
// shouldComponentUpdate to determine if the component needs to be updated
constshouldUpdate = checkShouldComponentUpdate(workInProgress, ctor, ...) ;if (shouldUpdate) {
// Execute the UNSAFE_componentWillUpdate life cycle function if updates are required
instance.componentWillUpdate(newProps, newState, nextContext);
// And add effect. In the COMMIT phase, componentDidUpdate, getSnapshotBeforeUpdate are executed
workInProgress.effectTag |= Update;
workInProgress.effectTag |= Snapshot;
}
// Update props and state
instance.props = newProps;
instance.state = newState;
return shouldUpdate;
}
Copy the code
The code snippet above is simplified code, such as before calling a lifecycle function or adding effect. React uses the Typeof operator to check if the component implements this method. React, for example, checks whether componentDidUpdate is implemented before Effect is added
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.effectTag |= Update;
}
Copy the code
Ok, so now we know what the ClickCounter Fiber node does in the Render phase. Now let’s look at how the values on the Fiber node are changed. When React starts working, the ClickCounter component has the following Fiber nodes:
{
effectTag: 0.elementType: class ClickCounter.firstEffect: null.memoizedState: {count: 0},
type: class ClickCounter.stateNode: {
state: {count: 0}},updateQueue: {
baseState: {count: 0},
firstUpdate: {
next: {
payload: (state, props) = >{... }}},... }}Copy the code
After the work is done, we have the following Fiber nodes
{
effectTag: 4.elementType: class ClickCounter.firstEffect: null.memoizedState: {count: 1},
type: class ClickCounter.stateNode: {
state: {count: 1}},updateQueue: {
baseState: {count: 1},
firstUpdate: null. }}Copy the code
Look at the contrast between the two. MemoizedState property value changed from 0 to 1. UpdateQueue’s base Estate property value changed from 0 to 1. UpdateQueue has no queue update and firstUpdate is null. And we changed the value of the effectTag to mark the side effects that we performed during the COMMIT phase.
The effectTag changed from 0 to 4, which in binary is 0b00000000100, which means the third bit was set and this bit represents Update
export const Update = 0b00000000100;
Copy the code
In conclusion, the Fiber node of the ClickCounter component calls the pre-mutation lifecycle method during the Render phase, updates the state and defines the associated side effects.
Sub-coordination of ClickCounter Fiber
With that done, React goes to the finishClassComponent function, which is where React calls the render method of the component and applies the diff algorithm to its children. The React documentation provides a general overview of the DIff algorithm.
If we dig deeper, we can see that the Diff algorithm in React actually compares the React element to the Fiber node. The process is extremely complex (which the author does not elaborate on here). In our example, the Render method returns the React element array, so if you want more details, check out the React source reconcileChildrenArray function.
There are two important things to understand at this point.
- React creates or updates the Fiber node of the child element during the subcoordination process. The child element is returned by render. The reference to the first child of the current node returned by the finishClassComponent method. The reference is assigned to the nextUnitOfWork variable and processed in the workLoop.
- React updates the props for the child, which is part of the parent’s work. To do this, it uses data from the React element returned from the Render method.
For example, before React performs sub-coordination, span corresponds to the Fiber node
{
stateNode: new HTMLSpanElement,
type: "span".key: "2".memoizedProps: {children: 0}, // Props for rendering last time
pendingProps: {children: 0}, // The updated props needs to be used on dom and child components. }Copy the code
When creating a Fiber backup, createWorkInProgress will synchronize the updated data of the React element to the Fiber node. (All the work on Fiber is done on the backup node)
{
$$typeof: Symbol(react.element)
key: "2"
props: {children: 1}
ref: null
type: "span"
}
Copy the code
After the ClickCounter component completes its sub-coordination, the Fiber node of the SPAN element will be updated as follows:
{
stateNode: new HTMLSpanElement,
type: "span".key: "2".memoizedProps: {children: 0}, // The Fiber props used to create the output during the last render.
pendingProps: {children: 1}, // Updated Fiber props Needs to be used for child components and DOM elements.. }Copy the code
Later, when working on the Fiber node of the SPAN element, we copy the pendingProps to memoizedProps and add effects to update the DOM during the COMMIT phase.
Ok, that’s all the ClickCounter component does in the Render phase. Since the button element is the first child of the ClickCounter component, all the Fiber nodes of the button element are assigned to nextUnitOfWork. React assigns its sibling, the Span element’s Fiber node, to nextUnitOfWork because the Button element’s Fiber node is not working. The behavior here is in the completeUnitOfWork method.
Handle span Fiber updates
NextUnitOfWork currently points to the Span Fiber standby node (because all the work is done on the workInProgress Tree). The steps are similar to ClickCounter, starting with the beginWork function.
The Fiber node with the SPAN element has a tag attribute of type HostComponent, and the beginWork enters the updateHostComponent branch. (This section conflicts with the current React version)
function beginWork(current$$1, workInProgress, ...) {...switch (workInProgress.tag) {
caseFunctionalComponent: {... }caseClassComponent: {... }case HostComponent:
returnupdateHostComponent(current, workInProgress, ...) ;case. }Copy the code
Subcoordination of SPAN Fiber
In our example, nothing important happens in the sub-coordination of span Fiber
Span Fiber gets the job done
Once the beginWork is complete, Span Fiber enters the completeWork. But before React can do that, they need to update memoizedProps on Span Fiber. React updates the Span Fiber pendingProps field during sub-coordination. (This part conflicts with the current React version)
{
stateNode: new HTMLSpanElement,
type: "span".key: "2".memoizedProps: {children: 0},
pendingProps: {children: 1},... }Copy the code
Once the beginWork of SPAN Fiber is complete, the pendingProps will be updated to memoizedProps
function performUnitOfWork(workInProgress) {... next = beginWork(current$$1, workInProgress, nextRenderExpirationTime); workInProgress.memoizedProps = workInProgress.pendingProps; . }Copy the code
Then you call the completeWork method, which also has a big switch statement inside it
function completeWork(current, workInProgress, ...) {...switch (workInProgress.tag) {
caseFunctionComponent: {... }caseClassComponent: {... }caseHostComponent: { ... updateHostComponent(current, workInProgress, ...) ; }case. }}Copy the code
Since span Fiber is a HostComponent, the updateHostComponent function is executed. In this function React does the following:
- Prepare DOM updates
- They are added to the Update Ue of Span Fiber
- Add effects for DOM updates
Before performing these operations, the Fiber node structure:
{
stateNode: new HTMLSpanElement,
type: "span".effectTag: 0
updateQueue: null. }Copy the code
Structure of Fiber node after operation:
{
stateNode: new HTMLSpanElement,
type: "span".effectTag: 4.updateQueue: ["children"."1"],... }Copy the code
Note that the effectTag value of the Fiber node has changed from 0 to 4, which is 100 in binary. This is the bit representing the Update side effect. This is what the React phase needs to do.
React completes the Render phase after completing the child element work and ClickCounter work in sequence. React assigns the workInProgress Tree (the backup node updated during the Render phase) to the finishedWork property of the FiberRoot node. This is a new tree that needs to be refreshed on the screen, which can be processed immediately after the Render phase or suspended for the browser’s free time.
effects list
In our example, the SPAN and ClickCounter nodes have side effects. The firstEffect property on HostRoot (a node of the Fiber tree) points to the Span Fiber node.
React creates an Effects List in the compliteUnitOfWork function. This is a Fiber tree with effects. Effects contains: update the span text and call the ClickCounter lifecycle function.
Effects list
The commit phase
The COMMIT phase starts with the completeRoot function, which sets the FiberRoot finishedWork property to NULL before starting any work.
The COMMIT phase is always synchronous, so it can safely update HostRoot to indicate that the COMMIT has started.
The COMMIT phase is where React updates the DOM and calls lifecycle methods. React will do this by iterating through the Effects list constructed in the previous Render phase and applying them.
In the Render phase, the effects of Span and ClickCounter are as follows:
{ type: ClickCounter, effectTag: 5 }
{ type: 'span'.effectTag: 4 }
Copy the code
The ClickCounter Fiber node has the effectTag value of 5 and the binary value of 5 is 101. The third bit is set to 1. This is the Update side effect bit and we need to call the componentDidUpdate lifecycle method. The lowest level is also 1, with all work done on the surface Fiber node in the Render phase.
The span Fiber node has the effectTag value of 5 and the binary value of 5 is 101, and the third bit is set to 1. This is the Update bit. We need to Update the textContent of the Span element.
The application effects
Effects is applied in the function commitRoot, which consists of three submethods.
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
Copy the code
Each child method loops through the Effects List and checks the type of effects.
The first function commitBeforeMutationLifeCycles check the Snapshot effects, and the getSnapshotBeforeUpdate function is called, But we didn’t add the getSnapshotBeforeUpdate lifecycle function in the ClickCounter component, so React doesn’t add it in the Render phase, so in our example, this method does nothing.
DOM updates
Next comes commitAllHostEffects, where the text content of the span goes from 0 to 1. The ClickCounter Fiber node has no action.
CommitAllHostEffects is also a large switch inside the commitAllHostEffects that applies the appropriate actions, depending on the type of effects:
function updateHostEffects() {
switch (primaryEffectTag) {
casePlacement: {... }casePlacementAndUpdate: {... }case Update:
{
var current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
caseDeletion: {... }}}Copy the code
Go to the commitWork function, and most recently to the updateDOMProperties method, which uses the payload added to the updateQueue property on the Fiber node during the Render phase, And apply it to the textContent property of the SPAN element.
function updateDOMProperties(domElement, updatePayload, ...) {
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if(propKey === STYLE) { ... }else if(propKey === DANGEROUSLY_SET_INNER_HTML) {... }else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {...}
}
Copy the code
After the DOM update is applied, React assigns the workInProgress tree on finishedWork to HostRoot. Set workInProgress tree to Current Tree.
root.current = finishedWork;
Copy the code
Call the post-lifecycle function
The last remaining function is commitAllLifecycles. During the Render phase React adds Update Effects to the ClickCounter component. The commitAllLifecycles function is where the post-mutation lifecycle method is called.
function commitAllLifeCycles(finishedRoot, ...) {
while(nextEffect ! = =null) {
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
constcurrent = nextEffect.alternate; commitLifeCycles(finishedRoot, current, nextEffect, ...) ; }if(effectTag & Ref) { commitAttachRef(nextEffect); } nextEffect = nextEffect.nextEffect; }}Copy the code
The lifecycle functions are called in the commitLifeCycles function.
function commitLifeCycles(finishedRoot, current, ...) {...switch (finishedWork.tag) {
caseFunctionComponent: {... }case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
instance.componentDidMount();
} else{... instance.componentDidUpdate(prevProps, prevState, ...) ; }}}caseHostComponent: {... }case. }Copy the code
As you can see in this method, React calls the componentDidMount method for the first rendered component.
conclusion
Max Koretskyi’s article is quite extensive, so to sum up the knowledge points:
- Each component has one associated with it
updater
(Updater). The updater acts as a component andReact core
Bridge between. This allows setState to be implemented in different ways in ReactDOM, React Native, server-side rendering, and test cases. - For class components,
updater
(Updater) yesclassComponentUpdater - When updating,
updater
It will be in the Fiber nodeupdateQueue
Property to add an update queue. render
(Render) phase, React will be fromHostRoot
Start traversing the Fiber tree, skipping the Fiber nodes that have already been processed until you find morework
No completed Fiber node. All work is done on the backup of the Fiber node, which is stored on the Fiber nodealternate
On the field. ifalternate
The fields have not been created yet and React will be used before processing workcreateWorkInProgress
createalternate
Fields,createWorkInProgress
The React function synchronizes the state of the React element to the Fiber node.nextUnitOfWork
To keep theworkInProgress tree
A reference to a Fiber node that has work to deal with in.- Fiber node access
beginWork
The function,beginWork
Function will perform corresponding work according to Fiber node type, class component will beupdateClassComponent
Function execution. - Each Fiber node executes
beginWork
Function. afterbeginWork
Function, the component is either created as a component instance, or simply updated as a component instance. - after
beginWork
.updateClassComponent
After enteringupdateClassInstance
Here is the processing class component for the most partwork
Place. (The following operations are performed in sequence)- Executive function UNSAFE_componentWillReceiveProps life cycle
- Execute the updateQueue for updateQueue in the Fiber node to generate the new state
- With the new state, execute getDerivedStateFromProps and get the result returned
- ShouldComponentUpdate to determine if the component needs to be updated. If false, skip the entire Render processing, including this component and its children. If true, continue updating.
- Execute the UNSAFE_componentWillUpdate life cycle function
- Add Effects to trigger the componentDidUpdate lifecycle function (
commit
Stages are triggered.) - Update the state and props on the component instance
- The Render phase class component mainly does: call the pre-mutation lifecycle method, update state, and define relevant effects.
- complete
updateClassInstance
After that, React entersfinishClassComponent
Where React calls the render method of the component and applies the diff algorithm to its children. - The subcoordinator creates or updates the Fiber node of the subelement, which is returned by the Render method and whose attributes are synchronized to the Fiber node of the subelement.
finishClassComponent
The Fiber node of the first child element is returned and assigned tonextUnitOfWork
After convenienceworkLoop
The process continues (then the node of the child node). - Updating the child element props is part of the work done on the parent element.
- After the RENDER phase is complete. The React to
workInProgress tree
(Backup node, tree updated in render phase) assigned toFiberRoot
The node’sfinishedWork
Properties. commi
Before t,FiberRoot
thefinishedWork
Property set tonull
.commit
Phases are synchronous, where React updates the DOM and calls lifecycle methods (to apply side effects).
reference
- In-depth explanation of state and props update in React