Original link: medium.com/react-in-de…
Take a look inside React Fiber
In my previous article, Inside Fiber: An in-depth overview of React’s new coordination algorithm, I laid out the basics to understand the technical details of the update processing I’m explaining in this article.
I’ve already outlined the main data structures and concepts that will be used in this article, especially Fiber nodes, current and working process trees, side effects and action lists. I’ve also provided an overview of the main algorithms and explained the difference between the render and Commit phases. If you haven’t read it yet, I suggest you start there or
I also introduced a simple application of button that renders an increasing tree on the screen:
You can run it here and it implements a simple component that returns two child elements, Button and SPAN, via the ** Render method. When you click on a button, the component’s state is updated in the handler, and the span** element’s text is updated as a result:
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
Here I also added **componentDidUpdate to the component to demonstrate how React adds effects to call this method during the COMMIT phase.
In this article, I want to show you how React handles status updates and builds action lists. We’ll take you through the render and Commit phases to see what the methods do in general.
In particular, we’ll see how React works in completeWork:
- Update * *
ClickCounter
thestate
In thecount
* * attributes. - Call * *
render
** method to get the list of child nodes and perform the comparison. - Update * *
span
Props of the ** element
React on commitRoot:
- Update * *
span
Elements of thetextCount
* * attributes. - Call * *
componentDidUpdate
** Lifecycle methods.
Before we do that, let’s look at how work is called when we call **setState** in the click handler.
Note that you need to know how to use React. This article is about how React works internally.
Scheduling Updates
When we click on button, the **click** event is triggered. React performs a callback method passed to Button via props. In our case, it simply increments the counter and updates the 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, which acts as a bridge between the component and the React kernel. This allows setState to be implemented differently in ReactDOM, React Native, server rendering, and test tools.
In this article, we’ll look at the implementation of the updater object in the ReactDOM, which uses the Fiber coordinator. For the ClickCounter** component, it is the classComponentUpdater, which is responsible for fetching Fiber instances, queued updates, and scheduling.
When the updates are queued, they add a queue of updates to be processed on the Fiber node. In our example, the Fiber node responsible for the ClickCounter component has the following structure:
{
stateNode: new ClickCounter,
type: ClickCounter,
updateQueue: {
baseState: {count: 0}
firstUpdate: {
next: {
payload: (state) = > { return {count: state.count + 1}}}},... },... }Copy the code
. As you can see, * * updateQueue firstUpdate. Next. The methods on the content is in our ClickCounter component is passed to the setState callback, it said in the render phase * * need to deal with the first update.
Handle updates to ClickCounter Fiber nodes
The work loop section of my previous article explained the role of the **nextUnitOfWork global variable, in particular, it holds the Fiber node from the workInProgress** tree that still has work to do. Use it to know if there are other Fiber nodes with unfinished work when React traverses the Fiber tree.
We start by assuming that the **setState method is called. React adds the setState callback to ClickCounter and the call works. React enters the render phase. It uses the renderRoot method to iterate from the top-level HostRoot, then calls and processes the Fiber nodes until it finds a node that hasn’t finished its work. At this point, there’s only one fiber node that does work, the ClickCounter**Fiber node.
All work is performed on a copy of Fiber stored in the **alternate field. If the alternate node has not already been created, React creates the copy in the createWorkInProgress method before processing the update. Let’s assume that the nextUnitOfWork traversal holds a reference to this replica ClickCounter**Fiber node.
beginWork
First, we enter the beginWork method in Fiber.
Since this method is executed on every fiber node in the tree, this is a good place to break if you want to debug during the **render** phase. I often check fiber node types this way to determine which node I need.
The beginWork is basically a large switch statement that determines the type of work each Fiber needs to do based on the tag, and then executes the respective methods. In our CountClicks example, it’s a class component, so this part is executed:
function beginWork(current?1, workInProgress, ...) {
...
switch (workInProgress.tag) {
...
case FunctionalComponent: {...}
case ClassComponent:
{
...
return updateClassComponent(current?1, workInProgress, ...) ; }caseHostComponent: {... }case. }Copy the code
We go to the updateClassComponent method, depending on whether it’s first rendered, work resumes, or just updates. React either creates an instance and mounts the component, or just updates it:
function updateClassComponent(current, workInProgress, Component, ...) {... const instance = workInProgress.stateNode;let shouldUpdate;
if (instance === null) {...// In the initial pass we might need to construct the instance.constructClassInstance(workInProgress, Component, ...) ; mountClassInstance(workInProgress, Component, ...) ; shouldUpdate =true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...) ; }else{ shouldUpdate = updateClassInstance(current, workInProgress, ...) ; }returnfinishClassComponent(current, workInProgress, Component, shouldUpdate, ...) ; }Copy the code
Handle updates to ClickConter Fiber
We already have an instance of the **ClickCounter** component, so let’s go to updateClassInstance. This is where React does most of the work for the class component. There are the most important operations in the method, in order:
- Call UNSAFE_componentWillReceiveProps hooks (deprecated)
- Perform * *
updateQueue
**, and generate a new state - Call ** with the new state
getDerivedStateFromProps
**, and get results - Call * *
shouldComponentUpdate
Ensure that the component needs to be updated and, if not, skip the entire Render processing, including that component’s and its children’srender
** call, otherwise with update processing. - Call * *
UNSAFE_componentWillUpdate
** (deprecated) - Add an effect to trigger **
componentDidUpdate
** Lifecycle hooks
Although the function of calling **componentDidUpdate is added during the Render phase, this method will be executed during the Commit phase
- Update ** on the component instance
state
andprops
**
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 always return the same result.
Here’s a simpler version of this method:
function updateClassInstance(current, workInProgress, ctor, newProps, ...) {
const instance = workInProgress.stateNode;
const oldProps = workInProgress.memoizedProps;
instance.props = oldProps;
if(oldProps ! == newProps) { callComponentWillReceiveProps(workInProgress, instance, newProps, ...) ; }let updateQueue = workInProgress.updateQueue;
if(updateQueue ! = =null) { processUpdateQueue(workInProgress, updateQueue, ...) ; newState = workInProgress.memoizedState; } applyDerivedStateFromProps(workInProgress, ...) ; newState = workInProgress.memoizedState;constshouldUpdate = checkShouldComponentUpdate(workInProgress, ctor, ...) ;if (shouldUpdate) {
instance.componentWillUpdate(newProps, newState, nextContext);
workInProgress.effectTag |= Update;
workInProgress.effectTag |= Snapshot;
}
instance.props = newProps;
instance.state = newState;
return shouldUpdate;
}
Copy the code
I’ve removed some of the helper code in the code snippet above. For instance, React uses the Typeof operator to check whether the component implements the lifecycle methods before calling them or adding functions that trigger them. For example, here’s the React test **componentDidUpdate** before it’s added:
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.effectTag |= Update;
}
Copy the code
Ok, now that I know what ClickCounter has to do in the Render phase, let’s look at what those operations change on the Fiber node. When React starts working, the Fiber node for the ClickCounter** component looks like this:
{ 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 get the Fiber phase result that looks like this:
{ 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
Take a moment to observe the difference in property values
After the update is executed, the value of the count property becomes 1 on memoizedState and baseState in updateQueue, and React also updates the state in the **ClickCounter** component instance.
At this point, we no longer have updates in the queue, all **firstUpdate is null, and importantly, we’ve changed the value of the effectTag property, which is no longer 0, but 4, which in binary is 100, which means that the third bit is set, And this one represents the side-effect tag of Update** :
export const Update = 0b00000000100;
Copy the code
The job of the **ClickCounter**Fiber node is to call the pre-mutation lifecycle method, update the state and define the associated side effects.
Sub-coordination of ClickCounter Fiber
Once that’s done, React goes to finishClassComponent. In this method, React calls the component instance **render** and performs the diff algorithm on the child returned by the component, as outlined in this documentation. Here’s the relevant part:
When comparing two React DOM elements of the same type, React looks at the properties of both, preserves the consistent properties in the DOM node, and updates only the changing properties.
Then, if we dig a little deeper, we can see that it does compare Fiber nodes to React elements, but I won’t go into detail right now because it’s quite detailed. I’ll write a separate article on subcoordination.
If you’re curious about this detail yourself, consult the reconcileChildrenArray method, because in our example, the ** Render ** method returns the React element array.
There are two important things to understand at this point. First, when React performs child coordination, it creates or updates the Fiber node of the React element’s child, which is returned by the **render method. The finishClassComponent returns a reference to the first child of the current Fiber node. It will be assigned to nextUnitOfWork for processing later in the work loop; Second, React updates the child’s props as part of the work performed on its parent, so to do this, it uses the render** method to return the data on the React element.
For example, here’s what the Fiber node associated with the ** SPAN element looks like before React coordinates ClickCounter** Fiber:
{
stateNode: new HTMLSpanElement,
type: "span".key: "2".memoizedProps: {children: 0},
pendingProps: {children: 0},... }Copy the code
As you can see, the children property is 0 on memoizedProps and pendingProps. This is the render method of the span element that returns the React element structure:
{
? typeof: Symbol(react.element)
key: "2"
props: {children: 1}
ref: null
type: "span"
}
Copy the code
As you can see, the Fiber node is a little different from the props of the React element returned. In the createWorkInProgress method that creates a copy of the Fiber node, React copies the updated properties from the React element to the Fiber node.
So, after the React sub-coordination on the ClickCounter component is complete, the spanFiber node’s pendingProps will be updated to match the values in the SPAN **React element:
{
stateNode: new HTMLSpanElement,
type: "span".key: "2".memoizedProps: {children: 0},
pendingProps: {children: 1},... }Copy the code
Then, when React is going to perform the work on the spanFiber node, it will copy them to memoizedProps** and add effects to update the DOM.
Well, that’s all React does on the **ClickCounterFiber node during the Render phase. Since Button is the first child on the ClickCounter component, it will assign to the nextUnitOfWork variable, and since there’s nothing to do, So React will move to its sibling spanFiber node, which happens in the completeUnitOfWork** method, according to the algorithm described here.
Span Fiber update processing
So, the **nextUnitOfWork variable now points to the span copy and React still works on it, similar to the steps on ClickCounte**, where we start with the beginWork method.
Since our **span node is of type HostComponent**, this time in the switch statement React executes this part:
function beginWork(current?1, workInProgress, ...) {... switch (workInProgress.tag) {caseFunctionalComponent: {... }caseClassComponent: {... }case HostComponent:
returnupdateHostComponent(current, workInProgress, ...) ;case. }Copy the code
Going to the updateHostComponent method, you can compare the class component’s call to the **updateClassComponent method, the method component’s updateFunctionComponent, and more. You can find all of these methods in the ReactFiberBeginwork.js file.
Subcoordination of SPAN fiber
In our example, nothing important happens in the updateHostComponent** at the **span node.
The span Fiber node is complete
Once the **beginWork completes, the node goes into the completeWork method, but before React needs to update memoizedProps on Span Fiber, as you may remember, when the ClickCounter component is subcoordinated, React Updates the spanFiber node pendingProps** :
{
stateNode: new HTMLSpanElement,
type: "span".key: "2".memoizedProps: {children: 0},
pendingProps: {children: 1},... }Copy the code
So, once the beginWork on spanFiber is complete, React is an update to memoizedProps** :
function performUnitOfWork(workInProgress) {... next = beginWork(current?1, workInProgress, nextRenderExpirationTime); workInProgress.memoizedProps = workInProgress.pendingProps; . }Copy the code
It then calls the **completeWork method, which is basically a big switch statement similar to what we saw in beginWork** :
function completeWork(current, workInProgress, ...) {... switch (workInProgress.tag) {caseFunctionComponent: {... }caseClassComponent: {... }caseHostComponent: { ... updateHostComponent(current, workInProgress, ...) ; }case. }}Copy the code
Since our spanFiber node is HostComponent**, it performs the updateHostComponent method, in which React basically does the following:
- Prepare DOM updates
- Add them to **
span
The fiberupdateQueue
** - Add the ability to update the DOM
Before the operation, the ** SPAN **Fiber node looks like:
{
stateNode: new HTMLSpanElement,
type: "span".effectTag: 0
updateQueue: null. }Copy the code
When the work is done, it will look something like:
{
stateNode: new HTMLSpanElement,
type: "span".effectTag: 4.updateQueue: ["children"."1"],... }Copy the code
Note that the effectTag and updateQueue fields are not 0, but **4. The binary is 100, which means that the third bit is set, and the third bit represents the Update side tag. This is the only thing that React needs to do in the commit phase. The payload held by the updateQueue** field will be used during updates.
Once React has processed **ClickCounter and their children, it has completed the Render phase and can now assign the completed copy (or alternate-alternate) tree to the finishedWork property on FiberRoot. This is a new tree that needs to be refreshed on the screen, which can be processed immediately after the Render ** phase or suspended while the browser gives React idle time.
Effects List
In our example, React points The firstEffect on HostFiber to the spanFiber node because both the SPAN node and the ClickCounter component have side effects.
React builds a list of actions in the compliteUnitOfWork method. Here is a Fiber tree with actions that update the text of the **span node and call the ClickCounter** hook:
Here is a linear list of active nodes:
The Commit phase
This phase starts with the completeRoot method, which sets the finishedWork property on **FiberRoot to NULL ** before it does any work:
root.finishedWork = null;
Unlike the render phase, the Commit phase is always synchronized, so it is safe to update HostRoot** to indicate that the commit has begun.
The **commit phase is where React updates the DOM and invokes the post-mutation lifecycle method componentDidUpdate. In order to do so, it iterates over the effects list created in the Render phase and applies it.
In the **render phase, we do the following for span and ClickCounter** nodes:
{ type: ClickCounter, effectTag: 5 }
{ type: 'span'.effectTag: 4 }
Copy the code
ClickCounter has a label value of 5 or binary 101, and the ** update defined is considered to call the class component’s componentDidUpdate lifecycle method. The lowest value is also set to indicate that all work is done for the Fiber node in the Render ** phase.
The span function tag is either 4 or binary 100, and the ** update defined is the DOM update of the host component. In our case, the React element will need to update the textContent** of the element.
Applying Effects
Here’s how React applies these effects: commitRoot methods, which apply these effects, are composed of three submethods:
function commitRoot(root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects();
root.current = finishedWork;
commitAllLifeCycles();
}
Copy the code
Each submethod iterates through the list of effects, checks for the type of action, and applies it when it finds an action for the purpose of the aspect. In our case, it calls the componentDidUpdate lifecycle method of the **ClickCounter component and updates the text content on the SPAN ** element.
First put commitBeforeMutationLifeCycles looking for * * the Snapshot function, and call getSnapshotBeforeUpdate method, however, because we did not implement this method in the ClickCounter components, React does not add this function to the render phase, so in our case, the method does nothing.
DOM updates
React then executes the commitAllHostEffects method, where React changes the text content of the **span element from 0 to 1, while ClickCounter** Fiber does nothing because there are no DOM updates for the nodes of the class component.
The approach is basically to select the right action type and apply the relevant actions. In my example, we need to Update the text of the **span element, so we go to the Update** section:
function updateHostEffects() {
switch (primaryEffectTag) {
casePlacement: {... }casePlacementAndUpdate: {... }case Update:
{
var current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
caseDeletion: {... }}}Copy the code
Going down to the commitWork, we’ll go to the updateDOMProperties method, which will fetch the payload of the updateQueue added to the Fiber node during the Render phase, And update 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 DOM updates are applied, React assigns the **finishedWork tree to HostRoot**, setting the alternate tree to the current tree:
root.current = finishedWork;
Invokes the post-mutation lifecycle hook
The final method is the commitAllLifecycles method, where React calls the post-mutation lifecycle method. In the Render phase, React adds an Update function to the ClickCounter component, which is one of the things the commitAllLifecycles method is looking for, and then calls the componentDidUpdate method:
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
This method also updates refs, but we’re not using this functionality, so the commitLifeCycles method calls:
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 rendering to the component.
The forehead… Finished it.