The introduction

In the last article, we used a quick example of the create-react-app scaffolding to explain the implementation principles behind react.componentand react.pureComponent. We have also learned that the return value of the render method in the class component and the return value of the function definition component can be converted to a multi-level nested structure wrapped in the react. CreateElement method by using the Babel presets toolkit @babel/ preset-React. And based on the source code line by line analysis of the react. createElement method behind the implementation process and the ReactElement constructor member structure, finally according to the analysis results summarized several interviews may encounter or have encountered before the interview test points. The content of the previous article is relatively simple, mainly for this article and the subsequent task scheduling related content to lay a foundation, to help us better understand the meaning of the source code. This article combines the basic content of the previous article, starting from the entry point of component rendering reactdom.render method, step by step in-depth source code, uncover the realization principle behind the Reactdom.render method, if there is an error, please also point out.

Source code has a lot of judgment similar to __DEV__ variable control statement, used to distinguish between development environment and production environment, THE author in the process of reading the source code is not too concerned about these content, directly skipped, interested in small partners can study their own research.

render VS hydrate

The source code analysis for this series is based on Reactv16.10.2. To ensure consistent source code, it is recommended that you choose the same version. The location to download this version and why I chose this version can be found in the preparation section of the previous article. Create/react-app (SRC /index.js); create/react-app (SRC /index.js);

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App'; . ReactDOM.render(<App />, document.getElementById('root')); .Copy the code

This file is the main entry file of the project, the App component is the root component, and reactdom.render is the entry point from which we start analyzing the source code. The complete code for the ReactDOM object can be found in the following path:

packages -> react-dom -> src -> client -> ReactDOM.js
Copy the code

Then we navigate to line 632 and see that the ReactDOM object contains many of the methods we might have used, such as Render, createPortal, findDOMNode, Hydrate, unmountComponentAtNode, etc. We’ll focus on the Render method for the moment in this article, but take a quick look at the Hydrate method for comparison:

const ReactDOM: Object= {...@param Element represents a ReactNode, It can be a ReactElement object * @param container needs to mount the component to the DOM container in the page * @param callback needs to be executed after rendering */
  hydrate(element: React$Node, container: DOMContainer, callback:?Function) {
    invariant(
      isValidContainer(container),
      'Target container is not a DOM element.',); .// TODO: throw or warn if we couldn't hydrate?
    // Note that the first argument is null and the fourth argument is true
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      true,
      callback,
    );
  },

  /** * client render * @param Element represents a ReactElement object * @Param container needs to mount the component to the DOM container in the page * @param callback needs to execute the callback function after rendering */
  render(
    element: React$Element<any>,
    container: DOMContainer,
    callback:?Function,
  ) {
    invariant(
      isValidContainer(container),
      'Target container is not a DOM element.',); .// Note that the first argument is null and the fourth argument is false
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false, callback, ); },... };Copy the code

See, the first parameter of the render method is the ReactElement object we talked about in the previous article, so the content of the previous article is to lay a foundation here, to facilitate our understanding of the parameters. In fact, the Element field in almost any method argument in the source code can be passed in as an instance of ReactElement, which is obtained by the Babel compiler during compilation using the react.createElement method. The render method invokes the legacyRenderSubtreeIntoContainer to formally enter the rendering process, but the need to pay attention to the here is, When performing legacyRenderSubtreeIntoContainer, render method and hydrate method in the first parameter value is null, the fourth parameter values, to the contrary.

Then position code into line 570, enter legacyRenderSubtreeIntoContainer method of concrete implementation:

/** * Start building FiberRoot and RootFiber, then start updating * @param parentComponent, You can treat this as a null value for the first argument in * @param children reactdom.render () or reactdom.hydrate (), Can be interpreted as the second parameter in the root component * @param container reactdom.render () or reactdom.hydrate (), which DOM container the component needs to mount * @param forceHydrate indicates whether or not to fuse, The render method passes false and the hydrate method passes true * @param callback reactdom.render () or the third argument in reactdom.hydrate (), The callback function * @returns {*} */ that needs to be executed after the component is rendered
function legacyRenderSubtreeIntoContainer(parentComponent: ? React$Component
       
        , children: ReactNodeList, container: DOMContainer, forceHydrate: boolean, callback: ? Function,
       ,>) {...// TODO: Without `any` type, Flow says "Property cannot be accessed on any
  // member of intersection type." Whyyyyyy.
  // There is no _reactRootContainer on the container when it is first executed
  // For the first time, root must be undefined
  let root: _ReactSyncRoot = (container._reactRootContainer: any);
  let fiberRoot;
  if(! root) {// Initial mount
    Container._reactrootcontainer points to an instance of ReactSyncRoot
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    // root represents an instance of ReactSyncRoot with an _internalRoot method pointing to a fiberRoot instance
    fiberRoot = root._internalRoot;
    // callback represents the third parameter in reactdom.render () or reactdom.hydrate ()
    // Override callback to find the corresponding rootFiber via fiberRoot, and set the stateNode of the first child of rootFiber as this in the callback
    // We rarely write the third parameter, so we don't have to worry about it
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    // For the first mount, the update operation should not be batch, so the unbatchedUpdates method is executed first
    // In this method we will switch executionContext to LegacyUnbatchedContext
    // Change the context and then call updateContainer to update
    // Restore executionContext to its previous state after executing updateContainer
    unbatchedUpdates((a)= > {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    Container._reactRootContainer already has an instance of ReactSyncRoot
    fiberRoot = root._internalRoot;
    // The following control statement is consistent with the above logic
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Update
    // For non-first-time mounts, there is no need to call the unbatchedUpdates method
    // No longer need to switch executionContext to LegacyUnbatchedContext
    Instead, call updateContainer directly to perform the update operation
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}
Copy the code

The above code content is a little bit more, may not be easy to understand at first glance, we can not rush to see the full function content. The container._reactRootContainer will have no value when we first execute the reactdom. render method, so let’s focus on the first if statement:

if(! root) {// Initial mount
    Container._reactrootcontainer points to an instance of ReactSyncRootroot = container._reactRootContainer = legacyCreateRootFromDOMContainer( container, forceHydrate, ); . }Copy the code

Here by calling legacyCreateRootFromDOMContainer method its return value assigned to the container. The _reactRootContainer, our position code into line 517 under same file, Look at the legacyCreateRootFromDOMContainer concrete implementation:

/** * Create and return an instance of ReactSyncRoot * @param container reactdom.render () or the second argument in reactdom.hydrate (), The DOM container that the component needs to mount * @param forceHydrate requires force-fusion, the Render method passes false, and the Hydrate method passes true * @returns {ReactSyncRoot} */
function legacyCreateRootFromDOMContainer(container: DOMContainer, forceHydrate: boolean,) :_ReactSyncRoot {
  // Determine whether fusion is required
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  // In the case of client rendering, all elements in the container need to be removed
  if(! shouldHydrate) {let warned = false;
    let rootSibling;
    // Loop over each child node for deletion
    while((rootSibling = container.lastChild)) { ... container.removeChild(rootSibling); }}...// Legacy roots are not batched.
  // Return an instance of ReactSyncRoot
  // This instance has an _internalRoot attribute pointing to fiberRoot
  return new ReactSyncRoot(
    container,
    LegacyRoot,
    shouldHydrate
      ? {
          hydrate: true,} :undefined,); }/** * Determine whether to merge based on nodeType and attribute. * @param container DOM container * @returns {Boolean} */
function shouldHydrateDueToLegacyHeuristic(container) {
  const rootElement = getReactRootElementInContainer(container);
  return!!!!! ( rootElement && rootElement.nodeType === ELEMENT_NODE && rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME) ); }/** * Get the first child of the DOM container from the container * @param container DOM container * @returns {*} */
function getReactRootElementInContainer(container: any) {
  if(! container) {return null;
  }

  if (container.nodeType === DOCUMENT_NODE) {
    return container.documentElement;
  } else {
    returncontainer.firstChild; }}Copy the code

Among them in shouldHydrateDueToLegacyHeuristic approach, first of all, according to the container to get the first child node in the DOM container, The purpose of obtaining this child node is to distinguish between client rendering and server rendering by the nodeType of the node and whether it has ROOT_ATTRIBUTE_NAME. ROOT_ATTRIBUTE_NAME located on the packages/react – dom/SRC/Shared/DOMProperty js file, said data – reactroot properties. As we know, in server rendering, different from client rendering, Node service will generate a complete HTML string based on the matched route in the background, and then send the HTML string to the browser. The resulting HTML structure is simplified as follows:

<body>
    <div id="root">
        <div data-reactroot=""></div>
    </div>
</body>
Copy the code

There is no data-reactroot attribute in client rendering, so client rendering and server rendering can be distinguished. Htmlnodetype. js is the same as domproperty.js. The htmlNodetype. js file is the same as domproperty.js.

// represents the element node
export const ELEMENT_NODE = 1;
// represents a text node
export const TEXT_NODE = 3;
// represents the comment node
export const COMMENT_NODE = 8;
// represents the entire document, i.e. Document
export const DOCUMENT_NODE = 9;
// represents the document fragment node
export const DOCUMENT_FRAGMENT_NODE = 11;
Copy the code

With this analysis, it’s now easy to tell the difference between client-side rendering and server-side rendering, and if asked about the difference in the two modes in an interview, it’s easy to tell the difference at the source level and make the interviewer’s eyes shine. So, so far, it’s been pretty easy, right?

FiberRoot VS RootFiber

In this section, we will try to understand two confusing concepts: FiberRoot and RootFiber. These two concepts play a key role in the whole task scheduling process of React. If you do not understand these two concepts, the subsequent task scheduling process will be empty talk. Therefore, we must understand these two concepts. Next on the content of the section, then continue to analyze the residual content in legacyCreateRootFromDOMContainer method, at the end of the function body returned to a ReactSyncRoot instance, Going back to the reactdom.js file, we can easily find the ReactSyncRoot constructor:

/** * ReactSyncRoot constructor * @param Container DOM container * @param tag fiberRoot tag (LegacyRoot, BatchedRoot, ConcurrentRoot) * @param options Configuration information, available only for hydrate, otherwise undefined * @constructor */
function ReactSyncRoot(container: DOMContainer, tag: RootTag, options: void | RootOptions,) {
  this._internalRoot = createRootImpl(container, tag, options);
}

/** * Create and return a fiberRoot * @param Container DOM container * @param Tag fiberRoot node tag (LegacyRoot, BatchedRoot, ConcurrentRoot) * @param options Configuration information. This value is available only for hydrate. Otherwise, it is undefined * @RETURNS {*} */
function createRootImpl(container: DOMContainer, tag: RootTag, options: void | RootOptions,) {
  // Tag is either LegacyRoot or Concurrent Root
  // Determine if it is a hydrate mode
  consthydrate = options ! =null && options.hydrate === true;
  consthydrationCallbacks = (options ! =null && options.hydrationOptions) || null;
  
  Create a fiberRoot
  const root = createContainer(container, tag, hydrate, hydrationCallbacks);
  // Append an internal attribute to the Container that points to the rootFiber node corresponding to the Current attribute of fiberRoot
  markContainerAsRoot(root.current, container);
  if(hydrate && tag ! == LegacyRoot) {const doc =
      container.nodeType === DOCUMENT_NODE
        ? container
        : container.ownerDocument;
    eagerlyTrapReplayableEvents(doc);
  }
  return root;
}
Copy the code

From the above source code, we can see that the createRootImpl method creates a fiberRoot instance by calling the createContainer method and returns that instance and assigns it to the _internalRoot property, an internal member of the ReactSyncRoot constructor. We continue with the createContainer method to explore the full creation of fiberRoot, which is extracted into a related dependency package, the React-Reconciler package, that is the same class as the React-DOM package. And then positioning to react – the reconciler/SRC/ReactFiberReconciler. Js line 299:

/** * Internally call createFiberRoot to return a fiberRoot instance * @param containerInfo DOM container * @param tag FiberRoot tags (LegacyRoot, BatchedRoot, ConcurrentRoot) * @param Hydrate Determine if this is a Hydrate mode * @param hydrationCallbacks This object is possible only in hydrate mode, and contains two alternative methods: onthirsty and onDeleted * @RETURNS {FiberRoot} */
export function createContainer(containerInfo: Container, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks,) :OpaqueRoot {
  return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}

/** * Create fiberRoot and rootFiber and reference each other * @param containerInfo DOM container * @param tag FiberRoot tags (LegacyRoot, BatchedRoot, ConcurrentRoot) * @param Hydrate Determine if this is a Hydrate mode * @param hydrationCallbacks This object is possible only in hydrate mode, and contains two alternative methods: onthirsty and onDeleted * @RETURNS {FiberRoot} */
export function createFiberRoot(containerInfo: any, tag: RootTag, hydrate: boolean, hydrationCallbacks: null | SuspenseHydrationCallbacks,) :FiberRoot {
  Create a fiberRoot instance using the FiberRootNode constructor
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
  if (enableSuspenseCallback) {
    root.hydrationCallbacks = hydrationCallbacks;
  }

  // Cyclic construction. This cheats the type system right now because
  // stateNode is any.
  // Create the root node of the Fiber Tree by using the createHostRootFiber method
  // Note that the fiber node forms a fiber tree just like the DOM tree
  // Each DOM node or component generates a corresponding Fiber node (this process will be explained in a future article)
  // Plays a crucial role in the subsequent reconciliation stage
  const uninitializedFiber = createHostRootFiber(tag);
  // After creating rootFiber, the fiberRoot instance's current property points to the newly created rootFiber
  root.current = uninitializedFiber;
  // The stateNode attribute of rootFiber points to the fiberRoot instance for mutual reference
  uninitializedFiber.stateNode = root;
  // Finally the created fiberRoot instance is returned
  return root;
}
Copy the code

A complete FiberRootNode instance contains a number of useful properties that play a role in the task scheduling phase, and you can see the full FiberRootNode constructor implementation in the ReactFiberroot.js file (only some of the properties are listed here) :

/** * FiberRootNode constructor * @param containerInfo DOM container * @param Tag fiberRoot (LegacyRoot, BatchedRoot, ConcurrentRoot) * @param hydrate Check for hydrate mode * @constructor */
function FiberRootNode(containerInfo, tag, hydrate) {
  // Used to mark the type of fiberRoot
  this.tag = tag;
  // point to the currently active corresponding rootFiber node
  this.current = null;
  // Information about the DOM container associated with fiberRoot
  this.containerInfo = containerInfo; .// Whether fiberRoot is currently in Hydrate mode
  this.hydrate = hydrate; .// Only one task is maintained on each fiberRoot instance, which is stored in the callbackNode property
  this.callbackNode = null;
  // Priority of the current task
  this.callbackPriority = NoPriority; . }Copy the code

Part of the attribute information is shown above. Since there are too many attributes and they are not needed in this article, I will not list them one by one here. The remaining attributes and their annotation information have been uploaded to Github, and interested friends can view them by themselves. Now that you know the fiberRoot property structure, move on to the second half of the createFiberRoot method:

// The following code comes from the createFiberRoot method above
// Create the root node of the Fiber Tree by using the createHostRootFiber method
const uninitializedFiber = createHostRootFiber(tag);
// After creating rootFiber, the fiberRoot instance's current property points to the newly created rootFiber
root.current = uninitializedFiber;
// The stateNode attribute of rootFiber points to the fiberRoot instance for mutual reference
uninitializedFiber.stateNode = root;

// The following code comes from the reactFiber.js file
/** * Internally call createFiber to create a FiberNode instance * @param Tag fiberRoot (LegacyRoot, BatchedRoot, ConcurrentRoot) * @returns {Fiber} */
export function createHostRootFiber(tag: RootTag) :Fiber {
  let mode;
  // The following code dynamically sets the mode property of rootFiber based on the fiberRoot flag type
  // export const NoMode = 0b0000; = > 0
  // export const StrictMode = 0b0001; = > 1
  // export const BatchedMode = 0b0010; = > 2
  // export const ConcurrentMode = 0b0100;  => 4
  // export const ProfileMode = 0b1000; = > 8
  if (tag === ConcurrentRoot) {
    mode = ConcurrentMode | BatchedMode | StrictMode;
  } else if (tag === BatchedRoot) {
    mode = BatchedMode | StrictMode;
  } else{ mode = NoMode; }...Call the createFiber method to create and return a FiberNode instance
  // HostRoot indicates the root node of the Fiber tree
  // Other tag types can be found in the shared/ reactworktags.js file
  return createFiber(HostRoot, null.null, mode);
}

/** * Create and return a FiberNode instance * @param tag to mark fiber node types (all types are stored in the shared/ reactWorktags.js file) * @param pendingProps * @param key is used to uniquely identify a fiber node (especially in tabular data structures where an additional key attribute is required for each DOM node or component). This will come in handy later in the reconciliation phase.) * @param mode specifies the mode of the fiber node * @returns {FiberNode} */
const createFiber = function(tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode,) :Fiber {
  // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
  The FiberNode constructor is used to create a FiberNode instance, that is, a fiber node
  return new FiberNode(tag, pendingProps, key, mode);
};
Copy the code

We have successfully created a fiber node. As mentioned above, similar to the DOM tree structure, the fiber node also forms a fiber tree corresponding to the DOM tree structure, which is based on a single linked list tree structure. The fiber node we just created above can be used as the root node of the entire Fiber tree, namely the RootFiber node. At this stage, we don’t have to worry about all the attributes contained in a Fiber node, but we can pay a little attention to the following attributes:

/** * FiberNode constructor * @param tag is used to mark the type of fiber node * @param pendingProps represents the props data to be processed * @param key Used to uniquely identify a fiber node (especially in tabular data structures where an additional key attribute is required for each DOM node or component, which is useful during the subsequent reconciliation phase) * @param mode denotes the mode of the fiber node * @constructor */
function FiberNode(tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode,) {
  // Instance
  // This is used to mark the type of fiber node
  this.tag = tag;
  // Uniquely identifies a fiber node
  this.key = key; .// For the rootFiber node, the stateNode attribute points to the corresponding fiberRoot node
  // For child Fiber nodes, the stateNode attribute points to the corresponding component instance
  this.stateNode = null;

  // Fiber
  // The following attributes create a single-linked list tree structure
  // The return attribute always points to the parent node
  // The child attribute always points to the first child node
  // Sibling attribute always points to the first sibling node
  this.return = null;
  this.child = null;
  this.sibling = null;
  // Index indicates the index of the current fiber node
  this.index = 0; .// Represents the props data to be processed
  this.pendingProps = pendingProps;
  // Represents props data that has been stored previously
  this.memoizedProps = null;
  // indicates the update queue
  // For example, in common setState operations
  // The data that needs to be updated is stored in the updateQueue queue here for subsequent scheduling
  this.updateQueue = null;
  // Represents previously stored state data
  this.memoizedState = null; .// Represents the mode of the fiber node
  this.mode = mode;

  // Indicates the expiration time of the current update task, after which the update task will be completed
  this.expirationTime = NoWork;
  // Indicates the expiration time of the highest priority task in the subfiber node of the current fiber node
  // The value of this property is dynamically adjusted based on the priority of the tasks in the sub-fiber node
  this.childExpirationTime = NoWork;

  // This is used to point to another fiber node
  // These two fiber nodes reference each other using the alternate property, forming a double buffer
  // The fiber node pointed to by the alternate property is also called the workInProgress node in task scheduling
  this.alternate = null; . }Copy the code

Other useful attributes the author has written in the source code of the relevant annotations, interested friends can view the complete annotation information on Github to help understand. Of course, some of these attributes are still hard to understand at this stage, but that’s ok, we’ll break them down one by one in future articles and series. The main purpose of this section is to understand the confusing concepts of FiberRoot and RootFiber and the relationship between the two. At the same time, we need to pay special attention here that multiple fiber nodes can form a tree structure based on a single linked list. The connection between multiple fiber nodes can be established through their own return, child and Sibling attributes. To make it easier to understand the relationship between multiple Fiber nodes and their properties, let’s review the simple example from the previous article. In the SRC/app.js file, we changed the default root component App generated by the create-React-app scaffolding to the following form:

import React, {Component} from 'react';

function List({data}) {
    return (
        <ul className="data-list">
            {
                data.map(item => {
                    return <li className="data-item" key={item}>{item}</li>})}</ul>
    );
}

export default class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            data: [1.2.3]}; } render() {return (
            <div className="container">
                <h1 className="title">React learning</h1>
                <List data={this.state.data} />
            </div>); }}Copy the code

The resulting DOM structure looks like this:

<div class="container">
    <h1 class="title">React learning</h1>
    <ul class="data-list">
        <li class="data-item">1</li>
        <li class="data-item">2</li>
        <li class="data-item">3</li>
    </ul>
</div>
Copy the code

Based on the DOM structure combined with the above source code analysis process, we can finally try to draw a diagram to deepen the impression:

conclusion

This article is mainly on the basis of the content of the last article from scratch line by line analysis of the implementation principle of ReactDOM. Render method, the implementation process and call stack behind it is still very complex, oneself is also in the process of constant groping. This article mainly introduces two core concepts: FiberRoot and RootFiber. Only by understanding and distinguishing these two concepts can we better understand the Fiber architecture of React and the task execution process in the task scheduling phase. The process of reading source code is painful, but at the same time their gains are also huge, in order to avoid the article is too boring, or intend to divide the source content into a series of articles to separate interpretation, during the interval can be used to review the previous content, avoid eating a fat man instead of good effect.

Thank you for reading

If you think the content of this article is helpful to you, can you do a favor to pay attention to the author’s public account [front-end], every week will try to original some front-end technology dry goods, after paying attention to the public account can invite you to join the front-end technology exchange group, we can communicate with each other, common progress.

The article has been updated toMaking a blog, if your article is ok, welcome to STAR!

One of your likes is worth more effort!

Grow up in adversity, only continue to learn, to become a better yourself, with you!