This article is free translation and collation, if misleading, please give up reading. The original text.

preface

This article uses an example consisting of parent component and children Component to describe the process of transmitting functions to sub-components in fiber architecture when React is used.

The body of the

In my previous article, I delved into the React Fiber architecture for the Reconciliation algorithm and mentioned that to understand the technical details of the update process, we need to have some basic knowledge. And this part of the basic knowledge is the content of this article.

The data structures and concepts mentioned in this article were outlined in my previous article. These data structures and concepts mainly include:

  • fiber node
  • current tree
  • work-in-progress tree
  • side-effects
  • effects list

At the same time, I also gave a macro description of the main algorithms and explained the differences between the Render stage and the COMMIT stage. If you haven’t read about these things, I suggest you do.

I also introduced a simple demo. The main function of this demo is to add a number on the interface by clicking on a button.

You can play with it here. This demo implements a simple component. The render method of this component returns two child components: Button and SPAN. When you click a button on the interface, we update the component’s state in click’s event handler. As a result, the text content of the SPAN element on the interface is updated.

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 add a life cycle function for componentDidUpdate to the component. This is done to demonstrate how React adds effects and calls the method in the COMMIT phase.

In this article, I’ll show you how React handles state updates and builds effects Lists. We also give a brief explanation of the top-level functions in the Render and Commit phases.

In particular, let’s focus on the completeWork method:

  • updateClickCounterIn component statecountProperties.
  • Of the component instancerenderMethod to get the children list, and then perform the comparison.
  • Update props for the SPAN element.

And commitRoot methods:

  • Update the span elementtextContentProperties.
  • callcomponentDidUpdateThis life cycle function.

Before we dive into this stuff, let’s quickly go over how work is scheduled when we call setState in the Click event handler.

Scheduling updates

When we click the button on the interface, the Click event is triggered, and React executes the event callback we passed in as props. In our demo, the event callback simply updates the component’s state by increasing the count field value.

class ClickCounter extends React.Component {
    ...
    handleClick() {
        this.setState((state) => {
            return{count: state.count + 1}; }); }}Copy the code

Each React component has its own updater, which acts as a bridge between the component and React Core. This design makes it possible for multiple render (such as ReactDOM, React Native, Server side Rendering and testing utilities) to implement their own setState methods.

In this article, we’ll take a separate look at the implementation of the updater object in the ReactDOM. In this implementation, Fiber Reconciler is used. For a ClickCounter component, the updater object is the classComponentUpdater. It is responsible for: 1) retrieving the Fiber instance; 2) Will update the request to join the team; 3) Schedule work.

When we say “an update request was queued”, we mean that a setState callback is added to the Fiber node’s “updateQueue” queue for processing. Returning to this example, the data structure of the Fiber node corresponding to the ClickCounter component is as follows:

{
    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 content of reference points to the function is passed to the callback setState method. It represents the first “update request” to be processed in the Render phase.

Handles update requests on the ClickCounter Fiber node

I explained the role of the global variable nextUnitOfWork in my previous section on work loop. In particular, the section states that this global variable refers to Fiber nodes on workInProgresstree that have work to do. When React traverses the entire Fiber tree, it uses this global variable to determine if there are any Fiber nodes that haven’t completed their work.

Let’s start where the setState method has already been called. After the setState method is called, React passes the Callback we passed to setState to the ClickCounterfiber node, adding the callback to the Fiber node’s updateQueue object. Then, the work is scheduled. React enters the render phase from here. It calls renderRoot to traverse the entire Fiber node tree, starting with the topmost HostRoot. Although React starts at the topmost root node, it will discard fiber nodes that have already been processed and only process nodes that still have work to do. At this moment, we only have one fiber node to work on. This node is the ClickCounterfiber node.

The alternate field of the ClickCounterfiber node is used to hold a reference to a clone of the current Fiber node. The work on this clone is already done. This clone is called the alternate Fiber node for the current Fiber Node. If the Alternate Fiber node has not been created yet, React uses the createWorkInProgress function to perform the replication before processing the update request. For now, we assume that the variable nextUnitOfWork holds the alternate Fiber node reference to the current Fiber node.

beginWork

First, our fiber node will be passed to the beginWork function.

This function is called on every node in the Fiber Node tree. So, if you want to debug the Render phase, this is a good place to interrupt. I often do this by checking fiber node type to see if the current node is the one I want to follow.

The beginWork function is basically just one big switch statement. In the switch statement, beginWork calculates the type of work required by the current Fiber node according to the workInProgress tag value. The corresponding function is then executed to perform the work. In our demo, since ClickCounter is a class component, we will execute the following branch statement:

function beginWork(current$The $1, workInProgress, ...) {... switch (workInProgress.tag) { ...caseFunctionalComponent: {... }case ClassComponent:
        {
            ...
            return updateClassComponent(current$The $1, workInProgress, ...) ; }caseHostComponent: {... }case. }Copy the code

So, we’ll go to the updateClassComponent function. Depends on whether it is currently 1) the first rendering of the component, 2) whether work is being restored; The React update does two things:

  • Either create a new instance and mount the component;
  • Or just update it.
functionupdateClassComponent(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 will already have an instance we can reuse. shouldUpdate = resumeMountClassInstance(workInProgress, Component, ...) ; }else{ shouldUpdate = updateClassInstance(current, workInProgress, ...) ; }returnfinishClassComponent(current, workInProgress, Component, shouldUpdate, ...) ; }Copy the code

Processing updates for the ClickCounter Fiber

We have already created an instance for ClickCounter, so our execution will go to the updateClassInstance method. In this method, React performs most of the work of the class Component. Here are the most important actions performed by this method (listed in the order in which the code is executed) :

  • callUNSAFE_componentWillReceivePropsLifecycle functions (deprecated);
  • To deal withupdateQueueUpdate request and generate a new state value in
  • Call with a new state valuegetDerivedStateFromPropsAnd get the result of the call.
  • callshouldComponentUpdateTo make sure that a component really wants to update. If the call returns false, React skips the entire render process including the render method that calls the component instance and its subcomponent instances. Otherwise, go through the update process normally.
  • callUNSAFE_componentWillUpdateLifecycle functions (deprecated);
  • Put the life cycle functioncomponentDidUpdateAdd to an effect.

Although the “call componentDidUpdate” effect is added during the Render phase, the actual execution of this method is in the commit phase that follows.

  • Update the state and props values on the component instance.

The state and props values should be updated before the Render method is called. The react component update is essentially calling the render method of the component instance with the latest state and props. If we didn’t, the render method would return the same value every time it was called.

Here is a condensed version of the updateClassInstance 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; const shouldUpdate = 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 more minor code. For example, React uses the Typeof operator to check if the component implements a method before calling the lifecycle function and adding effect and triggering it. React checks whether the componentDidUpdate method is a function before adding an effect:

if (typeof instance.componentDidUpdate === 'function') {
    workInProgress.effectTag |= Update;
}
Copy the code

At this point, we know what the ClickCounter Fiber node needs to do during the Render phase. Let’s take a look at how these operations change the values associated with a Fiber node. When React starts work, the ClickCounter component’s corresponding fiber node 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

When the work is complete, the Fiber node for the ClickCounter component 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

Observe the difference between the attribute values of each fiber node. We will see that the count field property in memoizedState and baseState has changed to 1 after processing the update request. React also updated the state of the ClickCounter component instance.

Currently, we have no update requests in updateQueue, so the value of firstUpdate is null. It’s also important to note that our effectTag field has changed from 0 to 4. 4 is 100 in binary, and this is the value of the update side-effect tag:

export const Update = 0b00000000100;
Copy the code

So here’s a quick summary. When React executes work on a ClickCounterfiber node, React does the following:

  • Call the pre-mutation lifecycle method
  • Update the state value
  • Define the associated side-effects.

Reconciling children for the ClickCounter Fiber

When the summary mentioned above is complete, React execution will enter the finishClassComponent. In this function, React will call the Render method of the component instance and apply the diff algorithm to its child component instances (which the Render method returns). In this article, there is a high quality summary of the Diff algorithm:

When comparing two React DOM elements React Element is essentially a react element, but type is a STRING of the DOM type.) When a type is the same, React checks the difference between the attributes of the react element and that of the DOM node, and only updates the attributes that need to be changed.

If we dig a little deeper, we’ll see that the react Element is actually the fiber node. I won’t go into too much detail in this article because the process is quite complicated. I will focus on the process of child Reconciliation in a separate article.

If you’re in a hurry to find out the Details of Child Reconciliation, you can check out this reconcileChildrenArray function. Because in our demo, ClickCounter’s Render method returns an array of React elements.

There are two important things to understand right now. The first is that as the Child Reconciliation process executes, React creates or updates the corresponding Fiber node for the Child React element returned from the Render method. The finishClassComponent function returns a reference to the first child Fiber node of the current Fiber node. This reference will be assigned to nextUnitOfWork and will be used in the next loop of the work loop; The second thing is that React treats updates to the props of the child Fiber node as part of the parent Fiber node’s work. To do this, React uses the data from the React Element returned by the Render method.

For example, before reconcile the children of a ClickCounterfiber node with React, the span element corresponds to a fiber node that looks like this:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 0},
    pendingProps: {children: 0},
    ...
}

Copy the code

As you can see, the children property in memoizedProps and pendingProps has a value of 0. Here is the react element for the SPAN element returned by the Render method:

{$$typeof: Symbol(react.element)
    key: "2"
    props: {children: 1}
    ref: null
    type: "span"
}
Copy the code

As you can see, the props in a Fiber node are different from the props in the react Element returned. In createWorkInProgress, this difference applies to alternate Fiber node creation. React is a way to copy updated props from a React Element to alternate Fiber node.

When Reconcile reconcile the Children of the React ClickCounter component, the value of the Fiber Node pendingProps field corresponding to the SPAN element is updated. This will be the same as the props value of the React element corresponding to the SPAN element:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 0},
    pendingProps: {children: 1},
    ...
}
Copy the code

Later, React performs work on the Fiber node corresponding to the SPAN elements, which copies them to memoizedProps and adds an effect to DOM update.

At this point, we’ve covered all the work that the ClickCounterfiber node needs to do in the Render phase. Since the Button component is the first child of the ClickCounter component, its corresponding Fiber node will be assigned to the nextUnitOfWork variable. Because this Fiber node doesn’t have any work to do. So React moves to the fiber node corresponding to its tsibling-span element. According to the algorithm described here, the above process occurs in the completeUnitOfWork function.

Processing updates for the Span fiber

So, the nextUnitOfWork variable now refers to the alternate Fiber node corresponding to the SPAN element. React updates span Fiber nodes from here. The process is the same for ClickCounter Fiber node, starting with the beginWork function.

Since the SPAN node is of type HostComponent, this time we will enter the HostComponent branch:

function beginWork(current$The $1, workInProgress, ...) {... switch (workInProgress.tag) {caseFunctionalComponent: {... }caseClassComponent: {... }case HostComponent:
          returnupdateHostComponent(current, workInProgress, ...) ;case. }Copy the code

Finally, we’ll go to the updateHostComponent function. Above, you can see the updateClassComponent we mentioned in the ClickCounter Fiber node. React will execute updateFunctionComponent and so on. You can find the code to implement all of these functions in the reactFiberBeginwork.js file.

Reconciling children for the span fiber

In our demo, nothing important happens in the updateHostComponent function because the children of the SPAN node are too simple.

Completing work for the Span Fiber node

Once the beginWork completes, the current Fiber node is passed to the completeWork. In this example, the fiber node is the Span Fiber node. Before doing this, React needs to update the memoizedProps field value on the SPAN Fiber node. As you may remember, when React reconciledthe child of the ClickCounter component, it updated the pendingProps field on the Span Fiber node:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    key: "2",
    memoizedProps: {children: 0},
    pendingProps: {children: 1},
    ...
}
Copy the code

So once the beginWork function is called on a SPAN Fiber node, React updates the memoizedProps field to the pendingProps field:

function performUnitOfWork(workInProgress) {
    ...
    next = beginWork(current$The $1, workInProgress, nextRenderExpirationTime); workInProgress.memoizedProps = workInProgress.pendingProps; . }Copy the code

After the beginWork function is executed, React will execute the completeWork function. The implementation of this function is basically a big switch statement. This is similar to the beginWork switch statement:

functioncompleteWork(current, workInProgress, ...) {... switch (workInProgress.tag) {caseFunctionComponent: {... }caseClassComponent: {... }caseHostComponent: { ... updateHostComponent(current, workInProgress, ...) ; }case. }}Copy the code

Since our Span Fiber node (the react Element) is a HostComponent, we’ll go inside the updateHostComponent function. Inside this function, React basically does three things:

  • Prepare for DOM updates
  • Add the prepared results to the updateQueue field of the Span Fiber node;
  • adds the effect to update the DOM

Before performing this operation, a SPAN Fiber node looks like this:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    effectTag: 0
    updateQueue: null
    ...
}
Copy the code

When the work is done, the Span Fiber node looks like this:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    effectTag: 4,
    updateQueue: ["children"."1"],... }Copy the code

Notice the difference in effectTag and updateQueue field values. For the value of the effectTag, it is no longer 0, but 4. In binary it is 100, and the third bit is the binary for side-effect update. The current position is 1, indicating that the side-effect that needs to be performed after the Span Fiber node is update. This is the only thing React needs to do for a Span Fiber node during the commit phase. The value of the updateQueue field holds data for the update.

Once React handles the ClickCounterfiber node and its sub-fiber nodes, the Render phase is over. React assigns the resulting Alternate Fiber node tree to the finishedWork property of the FiberRoot object. This new Alternate Fiber node tree contains things that need to be flushed to the screen. It will be processed immediately after the Render phase or executed later in the browser’s free time allocated to React.

effects list

In our example, the Span Fiber node and ClickCounter Fiber node have side effects. React will add a link to the Span Fiber node that points to HostFiber’s firstEffect property

In function compliteUnitWork, React completes the construction of the Effect List. Here is the Fiber node tree with effect in this example. On this tree, there are two effects: 1) updating the text content of the SPAN node; 2) Call the ClickCounter component’s lifecycle function:

Here is a linear list of fiber nodes with effect:

The commit phase

This phase starts with the completeRoot function. Before proceeding, it first sets the FiberRoot finishedWork property value to null:

root.finishedWork = null;
Copy the code

Unlike the Render phase, the Commit phase is executed synchronously. Therefore, it can safely update HostRoot to indicate that commit work has begun.

The COMMIT phase is where React performs DOM operations and calls the post-mutation lifecycle method componentDidUpate. To achieve these goals, React iterates through the Effect list produced by the Render phase and applies the corresponding effects.

For this example, after the Render phase, we have the following effects:

{ type: ClickCounter, effectTag: 5 }
{ type: 'span', effectTag: 4 }
Copy the code

The ClickCounterfiber node has an effect tag of 5, or “101” in binary notation. The corresponding work is update. For class Component, the work is “translated” to the life cycle method componentDidUpdate. In binary “101”, the lowest digit is “1”, which means that all work of the current Fiber node is completed in the Render phase.

The Span Fiber node has an effect tag value of 4, or “100” in binary. This number represents “Update” work because the current SPAN Fiber node corresponds to the host Component type. This “update” work is more specifically “DOM update”. Returning to this example, “DOM update” more specifically refers to “updating the textContent property of the SPAN element.”

Applying effects

Let’s take a look at how React applies these effects. The function commitRoot is used to apply effect. It consists of three subfunctions:

function commitRoot(root, finishedWork) {
    commitBeforeMutationLifecycles()
    commitAllHostEffects();
    root.current = finishedWork;
    commitAllLifeCycles();
}
Copy the code

All three child functions iterate over the Effect list and check the effect type as they iterate. If they find the current effect relevant to the responsibility of their function, they apply it. In our example, specifically calling the componentDidUpdate lifecycle method on the ClickCounter component and updating the text content of the SPAN element.

The first child function commitBeforeMutationLifeCycles will find the snapshot type of effect, and call the getSnapshotBeforeUpdate method. Because we didn’t implement this method on the ClickCouner component, React didn’t add this effect to the corresponding Fiber node in the Render phase. So, in our example, this subfunction does nothing.

DOM updates

React then moves to commitAllHostEffects functions. It is in this function that React completes updating the text content of the SPAN element from “0” to “1”. This function has almost nothing to do with ClickCounter as a fiber node. Because the Fiber node corresponds to the Class Component, class Componnet does not require any direct DOM updates.

The general framework of this function is to perform different operations on different types of effects. In our example, we need to Update the text content of the SPAN element, so we go with the Update branch:

function updateHostEffects() {
    switch (primaryEffectTag) {
      casePlacement: {... }casePlacementAndUpdate: {... }case Update:
        {
          var current = nextEffect.alternate;
          commitWork(current, nextEffect);
          break;
        }
      caseDeletion: {... }}}Copy the code

Following the commitWork, we’ll eventually enter the updateDOMProperties function. In this function, it uses the payload we added to the updateQueue field of the Fiber node in the Render phase to update the textContent 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

React assigns the finishedWork tree to HostRoot after the DOM update effect is applied. It sets alternate Tree to current tree:

root.current = finishedWork;
Copy the code

Calling post mutation lifecycle hooks

We have one last commitAllLifecycles to speak. In this function, React calls all the post-Mutational lifecycle methods. In the Render phase, React adds an effect called “Update” to the ClickCounter component. This effect is what this function looks for. Once it’s found, React calls the componentDidUpdate method:

function commitAllLifeCycles(finishedRoot, ...) {
    while(nextEffect ! == null) { const effectTag = nextEffect.effectTag;if(effectTag & (Update | Callback)) { const current = nextEffect.alternate; commitLifeCycles(finishedRoot, current, nextEffect, ...) ; }if(effectTag & Ref) { commitAttachRef(nextEffect); } nextEffect = nextEffect.nextEffect; }}Copy the code

This function also updates refs, but because we didn’t use this feature in this example. So the corresponding part of the code (commitAttachRef(nextEffect)) It will not be executed. The call to the componentDidUpdate method occurs in the commitLifeCycles function:

functioncommitLifeCycles(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 by the way, this is where React calls the componentDidMount lifecycle method. However, this call time is during the component’s first mount.