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:

  1. Update the state property.
  2. Call the Render method and compare the child nodes.
  3. Update props for the React element.

And in the commitRoot function React:

  1. Update the textContent attribute of the element.
  2. 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):

  1. Executive function UNSAFE_componentWillReceiveProps life cycle
  2. Execute the updateQueue for updateQueue in the Fiber node to generate the new state
  3. With the new state, execute getDerivedStateFromProps and get the result
  4. 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.
  5. Execute the UNSAFE_componentWillUpdate life cycle function
  6. Add effect to trigger the componentDidUpdate lifecycle function
  7. 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.

  1. 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.
  2. 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:

  1. Prepare DOM updates
  2. They are added to the Update Ue of Span Fiber
  3. 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:

  1. Each component has one associated with itupdater(Updater). The updater acts as a component andReact coreBridge between. This allows setState to be implemented in different ways in ReactDOM, React Native, server-side rendering, and test cases.
  2. For class components,updater(Updater) yesclassComponentUpdater
  3. When updating,updaterIt will be in the Fiber nodeupdateQueueProperty to add an update queue.
  4. render(Render) phase, React will be fromHostRootStart traversing the Fiber tree, skipping the Fiber nodes that have already been processed until you find moreworkNo completed Fiber node. All work is done on the backup of the Fiber node, which is stored on the Fiber nodealternateOn the field. ifalternateThe fields have not been created yet and React will be used before processing workcreateWorkInProgresscreatealternateFields,createWorkInProgressThe React function synchronizes the state of the React element to the Fiber node.
  5. nextUnitOfWorkTo keep theworkInProgress treeA reference to a Fiber node that has work to deal with in.
  6. Fiber node accessbeginWorkThe function,beginWorkFunction will perform corresponding work according to Fiber node type, class component will beupdateClassComponentFunction execution.
  7. Each Fiber node executesbeginWorkFunction. afterbeginWorkFunction, the component is either created as a component instance, or simply updated as a component instance.
  8. afterbeginWork.updateClassComponentAfter enteringupdateClassInstanceHere is the processing class component for the most partworkPlace. (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 (commitStages are triggered.)
    • Update the state and props on the component instance
  9. The Render phase class component mainly does: call the pre-mutation lifecycle method, update state, and define relevant effects.
  10. completeupdateClassInstanceAfter that, React entersfinishClassComponentWhere React calls the render method of the component and applies the diff algorithm to its children.
  11. 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.finishClassComponentThe Fiber node of the first child element is returned and assigned tonextUnitOfWorkAfter convenienceworkLoopThe process continues (then the node of the child node).
  12. Updating the child element props is part of the work done on the parent element.
  13. After the RENDER phase is complete. The React toworkInProgress tree(Backup node, tree updated in render phase) assigned toFiberRootThe node’sfinishedWorkProperties.
  14. commiBefore t,FiberRootthefinishedWorkProperty set tonull.
  15. commitPhases 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