抹桥的博客
Language
Home
Archive
About
GitHub
Language
主题色
250
3037 words
15 minutes
How ReactDOM Renders Components to the DOM
2017-09-21

What happens when we call ReactDOM.render for the first time in a React project? Today, let’s trace this question from the perspective of the source code (focusing on the overall process rather than getting bogged down in details).

ReactDOMStackEntry#

First, we can find the render method in ReactDOM’s entry file, ReactDOMStackEntry.js. As you can see, the render method is provided by the ReactMount component.

var ReactDOMStack = {
  findDOMNode: findDOMNode,
  render: ReactMount.render,
  unmountComponentAtNode: ReactMount.unmountComponentAtNode,
  version: ReactVersion,

  /* eslint-disable camelcase */
  unstable_batchedUpdates: ReactGenericBatching.batchedUpdates,
  unstable_renderSubtreeIntoContainer: ReactMount.renderSubtreeIntoContainer,
  /* eslint-enable camelcase */

  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
    // For TapEventPlugin which is popular in open source
    EventPluginHub: require("EventPluginHub"),
    // Used by test-utils
    EventPluginRegistry: require("EventPluginRegistry"),
    EventPropagators: require("EventPropagators"),
    ReactControlledComponent: require("ReactControlledComponent"),
    ReactDOMComponentTree,
    ReactDOMEventListener: require("ReactDOMEventListener"),
    ReactUpdates: ReactUpdates,
  },
};

ReactMount.render#

  render: function(nextElement, container, callback) {
    return ReactMount._renderSubtreeIntoContainer(
      null,
      nextElement,
      container,
      callback,
    );
  },

It then calls the _renderSubtreeIntoContainer method. The core content of this method is as follows:

ReactMount._renderSubtreeIntoContainer#

_renderSubtreeIntoContainer: function(
    parentComponent,
    nextElement,
    container,
    callback,
  ) {
    callback = callback === undefined ? null : callback;
    if (!React.isValidElement(nextElement)) {
      ...
    }
    // 创建下一个 wrapped 元素
    var nextWrappedElement = React.createElement(TopLevelWrapper, {
      child: nextElement,
    });

    var nextContext = getContextForSubtree(parentComponent);
    // 拿到当前的顶层容器组件
    var prevComponent = getTopLevelWrapperInContainer(container);
    // 对于第一次 render 来说,prevComponent 为 null
    if (prevComponent) {
      var prevWrappedElement = prevComponent._currentElement;
      var prevElement = prevWrappedElement.props.child;
      if (shouldUpdateReactComponent(prevElement, nextElement)) {
        var publicInst = prevComponent._renderedComponent.getPublicInstance();
        var updatedCallback =
          callback &&
          function() {
            validateCallback(callback);
            callback.call(publicInst);
          };
        ReactMount._updateRootComponent(
          prevComponent,
          nextWrappedElement,
          nextContext,
          container,
          updatedCallback,
        );
        return publicInst;
      } else {
        ReactMount.unmountComponentAtNode(container);
      }
    }

    var reactRootElement = getReactRootElementInContainer(container);
    var containerHasReactMarkup =
      reactRootElement && !!internalGetID(reactRootElement);
    var containerHasNonRootReactChild = hasNonRootReactChild(container);

    var shouldReuseMarkup =
      containerHasReactMarkup &&
      !prevComponent &&
      !containerHasNonRootReactChild;
    var component = ReactMount._renderNewRootComponent(
      nextWrappedElement,
      container,
      shouldReuseMarkup,
      nextContext,
      callback,
    )._renderedComponent.getPublicInstance();
    return component;
  },

Here are a few methods:

  • getTopLevelWrapperInContainer
  • shouldUpdateReactComponent
  • _renderNewRootComponent

getTopLevelWrapperInContainer#

This method is used to retrieve the existing top-level container component. The relevant code is as follows.

function getTopLevelWrapperInContainer(container) {
  var root = getHostRootInstanceInContainer(container);
  return root ? root._hostContainerInfo._topLevelWrapper : null;
}
function getHostRootInstanceInContainer(container) {
  var rootEl = getReactRootElementInContainer(container);
  var prevHostInstance =
    rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl);
  return prevHostInstance && !prevHostInstance._hostParent
    ? prevHostInstance
    : null;
}

It calls two methods: getReactRootElementInContainer and ReactDOMComponentTree.getInstanceFromNode.

getReactRootElementInContainer

function getReactRootElementInContainer(container) {
  if (!container) {
    return null;
  }

  if (container.nodeType === DOCUMENT_NODE) {
    return container.documentElement;
  } else {
    return container.firstChild;
  }
}

ReactDOMComponentTree

This module has three methods:

  • precacheChildNodes stores the corresponding React instance on the DOM node.
  • getNodeFromInstance retrieves the corresponding DOM node from an instance.
  • getInstanceFromNode retrieves the corresponding instance from a DOM node.

shouldUpdateReactComponent#

Determines whether the component needs to be updated.

function shouldUpdateReactComponent(prevElement, nextElement) {
  var prevEmpty = prevElement === null || prevElement === false;
  var nextEmpty = nextElement === null || nextElement === false;
  if (prevEmpty || nextEmpty) {
    return prevEmpty === nextEmpty;
  }

  var prevType = typeof prevElement;
  var nextType = typeof nextElement;
  if (prevType === "string" || prevType === "number") {
    return nextType === "string" || nextType === "number";
  } else {
    return (
      nextType === "object" &&
      prevElement.type === nextElement.type &&
      prevElement.key === nextElement.key
    );
  }
}

The logic is as follows:

  • Returns true if both the previous and current elements are null.
  • If it’s a textComponent, it updates directly.
  • Otherwise, if it’s a DOM element or a React element, and both type and key are the same, it returns true and performs an update.

ReactMount._renderNewRootComponent#

This method is the core of _renderSubtreeIntoContainer, used to mount a new component into the DOM.

  _renderNewRootComponent: function(
    nextElement,
    container,
    shouldReuseMarkup,
    context,
    callback,
  ) {
    // 通过 instantiateReactComponent 拿到 React Component 组件实例
    var componentInstance = instantiateReactComponent(nextElement, false);

    if (callback) {
      componentInstance._pendingCallbacks = [
        function() {
          validateCallback(callback);
          callback.call(
            componentInstance._renderedComponent.getPublicInstance(),
          );
        },
      ];
    }

    // The initial render is synchronous but any updates that happen during
    // rendering, in componentWillMount or componentDidMount, will be batched
    // according to the current batching strategy.
    ReactUpdates.batchedUpdates(
      batchedMountComponentIntoNode,
      componentInstance,
      container,
      shouldReuseMarkup,
      context,
    );

    var wrapperID = componentInstance._instance.rootID;
    instancesByReactRootID[wrapperID] = componentInstance;

    return componentInstance;
  },

instantiateReactComponent#

Generates different React Components based on the passed parameters. Core code:

  if (node === null || node === false) {
    instance = ReactEmptyComponent.create(instantiateReactComponent);
  } else if (typeof node === 'object') {
    var element = node;
    var type = element.type;
    if (typeof type !== 'function' && typeof type !== 'string') {
      ...
    }

    // Special case string values
    if (typeof element.type === 'string') {
      instance = ReactHostComponent.createInternalComponent(element);
    } else if (isInternalComponentType(element.type)) {
      // This is temporarily available for custom components that are not string
      // representations. I.e. ART. Once those are updated to use the string
      // representation, we can drop this code path.
      instance = new element.type(element);

      // We renamed this. Allow the old name for compat. :(
      if (!instance.getHostNode) {
        instance.getHostNode = instance.getNativeNode;
      }
    } else {
      instance = new ReactCompositeComponentWrapper(element);
    }
  } else if (typeof node === 'string' || typeof node === 'number') {
    instance = ReactHostComponent.createInstanceForText(node);
  } else {
    invariant(false, 'Encountered invalid React node of type %s', typeof node);
  }

From the code, you can see that based on element.type, there are three methods to generate three different types of React component instances:

  • ReactHostComponent.createInternalComponent(element)
  • new ReactCompositeComponentWrapper(element)
  • ReactHostComponent.createInstanceForText(node)

Readers might notice that I’ve omitted the code instance = new element.type(element); which executes when isInternalComponentType(element.type) is true. This is because it’s a solution for React’s internal encapsulated components not expressed as strings, and we don’t need to concern ourselves with it. Let’s look at the three methods above; two of them call the ReactHostComponent module.

ReactHostComponent#

Core code:

var ReactHostComponentInjection = {
  // This accepts a class that receives the tag string. This is a catch all
  // that can render any kind of tag.
  injectGenericComponentClass: function (componentClass) {
    genericComponentClass = componentClass;
  },
  // This accepts a text component class that takes the text string to be
  // rendered as props.
  injectTextComponentClass: function (componentClass) {
    textComponentClass = componentClass;
  },
};

function createInternalComponent(element) {
  invariant(
    genericComponentClass,
    "There is no registered component for the tag %s",
    element.type,
  );
  return new genericComponentClass(element);
}

/**
 * @param {ReactText} text
 * @return {ReactComponent}
 */
function createInstanceForText(text) {
  return new textComponentClass(text);
}

It provides two methods to create components, and the implementations of these two component classes are injected from other modules. So, where exactly are they injected from? After some searching, it turns out they are injected in ReactDOMStackInjection.js. Let’s take a look at the code:

var ReactComponentEnvironment = require("ReactComponentEnvironment");
var ReactComponentBrowserEnvironment = require("ReactComponentBrowserEnvironment");
var ReactDOMComponent = require("ReactDOMComponent");
var ReactDOMComponentTree = require("ReactDOMComponentTree");
var ReactDOMEmptyComponent = require("ReactDOMEmptyComponent");
var ReactDOMTextComponent = require("ReactDOMTextComponent");
var ReactDefaultBatchingStrategy = require("ReactDefaultBatchingStrategy");
var ReactEmptyComponent = require("ReactEmptyComponent");
var ReactGenericBatching = require("ReactGenericBatching");
var ReactHostComponent = require("ReactHostComponent");
var ReactReconcileTransaction = require("ReactReconcileTransaction");
var ReactUpdates = require("ReactUpdates");

var findDOMNode = require("findDOMNode");
var getHostComponentFromComposite = require("getHostComponentFromComposite");

ReactGenericBatching.injection.injectStackBatchedUpdates(
  ReactUpdates.batchedUpdates,
);

ReactHostComponent.injection.injectGenericComponentClass(ReactDOMComponent);

ReactHostComponent.injection.injectTextComponentClass(ReactDOMTextComponent);

ReactEmptyComponent.injection.injectEmptyComponentFactory(
  function (instantiate) {
    return new ReactDOMEmptyComponent(instantiate);
  },
);

ReactUpdates.injection.injectReconcileTransaction(ReactReconcileTransaction);
ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);

ReactComponentEnvironment.injection.injectEnvironment(
  ReactComponentBrowserEnvironment,
);

findDOMNode._injectStack(function (inst) {
  inst = getHostComponentFromComposite(inst);
  return inst ? ReactDOMComponentTree.getNodeFromInstance(inst) : null;
});

As you can see, this module injects ReactDOMComponent and ReactDOMTextComponent via ReactHostComponent.injection. It also injects some other modules, which we will use later.

Here, ReactDOMComponent and ReactDOMTextComponent are the modules that truly generate DOM markup. Their content is extensive but the code is relatively simple, so we won’t go into detail here. In short, calling the mountComponent method of both these modules will generate DOM Markup. The difference is that ReactDOMComponent will produce markup with the following structure:

{
  node: node,
  children: [],
  html: null,
  text: null,
  toString
}

Whereas ReactDOMTextComponent will directly generate String type text to be rendered in the DOM.

ReactCompositeComponent#

The last type of component should be ReactCompositeComponentWrapper, but looking at these two snippets within instantiateReactComponent:

var ReactCompositeComponentWrapper = function (element) {
  this.construct(element);
};

Object.assign(
  ReactCompositeComponentWrapper.prototype,
  ReactCompositeComponent,
  {
    _instantiateReactComponent: instantiateReactComponent,
  },
);

When this.construct is called, it still invokes ReactCompositeComponent, which represents user-defined components.

ReactUpdates.batchedUpdates#

function batchedUpdates(callback, a, b, c, d, e) {
  ensureInjected();
  return batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
}

As you can see, ReactUpdates calls batchedUpdate via batchingStrategy. And batchingStrategy is also injected by ReactDOMStackInjection, as mentioned earlier.

ReactUpdates.injection.injectReconcileTransaction(ReactReconcileTransaction);
ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);

ReactUpdates injects two modules: ReactReconcileTransaction and ReactDefaultBatchingStrategy.

Let’s first look at ReactDefaultBatchingStrategy; we’ll discuss ReactReconcileTransaction when we encounter it later. Here’s its code:

var ReactUpdates = require("ReactUpdates");
var Transaction = require("Transaction");

var emptyFunction = require("fbjs/lib/emptyFunction");

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}

Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function () {
    return TRANSACTION_WRAPPERS;
  },
});

var transaction = new ReactDefaultBatchingStrategyTransaction();

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  /**
   * Call the provided function in a context within which calls to `setState`
   * and friends are batched such that components aren't updated unnecessarily.
   */
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {
      // 如果当前 updates 已经完成,那么直接调用 callback
      return callback(a, b, c, d, e);
    } else {
      // 通过事务的方式去调用 callback
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};

As you can see, the ReactUpdates.batchUpdates we called earlier actually invokes batchedUpdates here, and its logic is quite simple.

A transaction method is used here, which I understand as a “transaction.” This means that if an update is currently in progress, the callback is invoked in a transactional manner.

transaction#

Transactions are widely used in React’s source code to invoke a method in a transactional way.

It wraps a method with one or more wrappers, which are executed sequentially before and after the method call. The transaction ensures that both the initialize and close methods of the wrapper are executed, regardless of whether the method being executed succeeds, fails, or throws an error. Its source code explanation makes this clear:

 * <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>

Now, let’s return to ReactDefaultBatchingStrategy. There are two wrappers in the code:

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

Their initialize methods are both empty functions, and their close methods are:

  1. Sets the current update status to false.
  2. flushBatchedUpdates is a more complex method that won’t be elaborated on here. Its main purpose is to ensure all components are updated correctly (flushBatchedUpdates->ReactUpdates.runBatchedUpdates->ReactCompositeComponent.performUpdateIfNecessary).

Now, back to the code above:

ReactUpdates.batchedUpdates(
  batchedMountComponentIntoNode,
  componentInstance,
  container,
  shouldReuseMarkup,
  context,
);

We can see that batchedMountComponentIntoNode is called here to perform subsequent work.

function batchedMountComponentIntoNode(
  componentInstance,
  container,
  shouldReuseMarkup,
  context,
) {
  var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
    /* useCreateElement */
    !shouldReuseMarkup,
  );
  transaction.perform(
    mountComponentIntoNode,
    null,
    componentInstance,
    container,
    transaction,
    shouldReuseMarkup,
    context,
  );
  ReactUpdates.ReactReconcileTransaction.release(transaction);
}

The first assignment statement here uses ReactReconcileTransaction, another module injected into ReactUpdates, which is React’s reconciliation transaction module.

var SELECTION_RESTORATION = {
  initialize: ReactInputSelection.getSelectionInformation,
  close: ReactInputSelection.restoreSelection,
};

/**
 * Suppresses events (blur/focus) that could be inadvertently dispatched due to
 * high level DOM manipulations (like temporarily removing a text input from the
 * DOM).
 */
var EVENT_SUPPRESSION = {
  initialize: function() {
    var currentlyEnabled = ReactBrowserEventEmitter.isEnabled();
    ReactBrowserEventEmitter.setEnabled(false);
    return currentlyEnabled;
  },
  close: function(previouslyEnabled) {
    ReactBrowserEventEmitter.setEnabled(previouslyEnabled);
  },
};

/**
 * Provides a queue for collecting `componentDidMount` and
 * `componentDidUpdate` callbacks during the transaction.
 */
var ON_DOM_READY_QUEUEING = {
  initialize: function() {
    this.reactMountReady.reset();
  },
  close: function() {
    this.reactMountReady.notifyAll();
  },
};
...
var TRANSACTION_WRAPPERS = [
  SELECTION_RESTORATION,
  EVENT_SUPPRESSION,
  ON_DOM_READY_QUEUEING,
];
function ReactReconcileTransaction(useCreateElement) {
  this.reinitializeTransaction();
  this.renderToStaticMarkup = false;
  this.reactMountReady = CallbackQueue.getPooled();
  this.useCreateElement = useCreateElement;
}
...
var Mixin = {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
  ...
}
Object.assign(ReactReconcileTransaction.prototype, Transaction, Mixin);

PooledClass.addPoolingTo(ReactReconcileTransaction);

This is also called in a transactional manner, and it has three wrappers:

  • Selection Restoration: Minimizes disruption to the user’s selection range during updates.
  • Event Suppression: Suppresses unnecessary event dispatches, such as blur events caused by temporarily removing an input element.
  • On DOM Ready Queueing: Provides a queue for componentDidMount and componentDidUpdate callback functions during transaction execution.

The subsequent getPooled method is a technique that utilizes an instance pool to avoid unnecessary garbage collection (GC), which won’t be explained further here.

Next, mountComponentIntoNode is called using this transactional approach. Let’s examine this method in detail.

mountComponentIntoNode#

function mountComponentIntoNode(
  wrapperInstance,
  container,
  transaction,
  shouldReuseMarkup,
  context,
) {
  var markup = ReactReconciler.mountComponent(
    wrapperInstance,
    transaction,
    null,
    ReactDOMContainerInfo(wrapperInstance, container),
    context,
    0 /* parentDebugID */,
  );

  wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
  ReactMount._mountImageIntoNode(
    markup,
    container,
    wrapperInstance,
    shouldReuseMarkup,
    transaction,
  );
}

Here, we see the operation of the key variable markup, which is the object we ultimately render into the DOM. The markup is obtained via the ReactReconciler.mountComponent method. The source code for ReactReconciler.mountComponent is as follows:

  mountComponent: function(
    internalInstance,
    transaction,
    hostParent,
    hostContainerInfo,
    context,
    parentDebugID, // 0 in production and for roots
  ) {
    var markup = internalInstance.mountComponent(
      transaction,
      hostParent,
      hostContainerInfo,
      context,
      parentDebugID,
    );
    if (
      internalInstance._currentElement &&
      internalInstance._currentElement.ref != null
    ) {
      transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
    }
    return markup;
  },

It then calls internalInstance.mountComponent. Here, internalInstance is essentially the React Component instance obtained earlier via instantiateReactComponent.

// _renderNewRootComponent
var componentInstance = instantiateReactComponent(nextElement, false);

Here, nextElement is the React root element to be rendered.

// _renderSubtreeIntoContainer
var nextWrappedElement = React.createElement(TopLevelWrapper, {
  child: nextElement,
});

The implementation of TopLevelWrapper; pay attention to its render method.

var TopLevelWrapper = function () {
  this.rootID = topLevelRootCounter++;
};
TopLevelWrapper.prototype.isReactComponent = {};
TopLevelWrapper.prototype.render = function () {
  return this.props.child;
};
TopLevelWrapper.isReactTopLevelWrapper = true;

The this.props.child returned within its render method is nextElement, which is <App/> from our project’s entry point ReactDOM.render(<App/>,document.getElementById('root')).

Returning to internalInstance.mountComponent within ReactReconciler.mountComponent. As discussed earlier with instantiateReactComponent, we know there are three types of components returned:

  1. ReactDOMComponent
  2. ReactDOMTextComponent
  3. ReactCompositeComponent

The first two types are straightforward; they are native DOM elements and will ultimately render their corresponding Markup. However, ReactCompositeComponent is more complex, so we’ll only look at the key code:

//ReactCompositeComponent.mountComponent
    var Component = this._currentElement.type;

    var updateQueue = transaction.getUpdateQueue();

    // Initialize the public class
    var doConstruct = shouldConstruct(Component);
    var inst = this._constructComponent(
      doConstruct,
      publicProps,
      publicContext,
      updateQueue,
    );
    var renderedElement;
    if (!doConstruct && (inst == null || inst.render == null)) {
      renderedElement = inst;
      inst = new StatelessComponent(Component);
      this._compositeType = ReactCompositeComponentTypes.StatelessFunctional;
    } else {
...
    }
      markup = this.performInitialMount(
        renderedElement,
        hostParent,
        hostContainerInfo,
        transaction,
        context,
      );

First, inst is obtained. The call stack to get inst is: this._constructComponent -> this._constructComponentWithoutOwner.

// this._constructComponentWithoutOwner
...
   var Component = this._currentElement.type;

    if (doConstruct) {
      if (__DEV__) {
        return measureLifeCyclePerf(
          () => new Component(publicProps, publicContext, updateQueue),
          this._debugID,
          'ctor',
        );
      } else {
        return new Component(publicProps, publicContext, updateQueue);
      }
    }
...

A Component instance is generated based on _currentElement.type, where currentElement.type is either a class inheriting from React.Component or a pure functional component. Then renderElement is declared. For stateless (functional) components, renderElement = inst; otherwise, it’s undefined.

Next, let’s look at performInitialMount.

if (renderedElement === undefined) {
  renderedElement = this._renderValidatedComponent();
}

var nodeType = ReactNodeTypes.getType(renderedElement);
this._renderedNodeType = nodeType;
var child = this._instantiateReactComponent(
  renderedElement,
  nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */,
);
this._renderedComponent = child;

var markup = ReactReconciler.mountComponent(
  child,
  transaction,
  hostParent,
  hostContainerInfo,
  this._processChildContext(context),
  debugID,
);

For non-stateless components, renderedElement needs to be assigned. The call stack is: this._renderValidatedComponent -> this._renderValidatedComponentWithoutOwnerOrContext.

  _renderValidatedComponentWithoutOwnerOrContext: function() {
    var inst = this._instance;
    var renderedElement;

    if (__DEV__) {
      renderedElement = measureLifeCyclePerf(
        () => inst.render(),
        this._debugID,
        'render',
      );
    } else {
      renderedElement = inst.render();
    }

    if (__DEV__) {
      // We allow auto-mocks to proceed as if they're returning null.
      if (renderedElement === undefined && inst.render._isMockFunction) {
        // This is probably bad practice. Consider warning here and
        // deprecating this convenience.
        renderedElement = null;
      }
    }

    return renderedElement;
  },

Ultimately, renderedElement is the result of inst.render().

Moving on, child = this._instantiateReactComponent(renderedElement) and markup = ReactReconciler.mountComponent(child,...).

It can be inferred that ReactReconciler.mountComponent will be called repeatedly and recursively here until child is no longer a ReactCompositeComponent, yielding the final Markup. The data structure of Markup can be found in DOMLazyTree:

// DOMLazyTree
{
  node: node,
  children: [],
  html: null,
  text: null,
  toString,
}

Once the Markup is obtained, only the final step remains: mounting the Markup into the actual DOM via ReactMount._mountImageIntoNode.

ReactMount._mountImageIntoNode#

  _mountImageIntoNode: function(
    markup,
    container,
    instance,
    shouldReuseMarkup,
    transaction,
  ) {
    invariant(
      isValidContainer(container),
      'mountComponentIntoNode(...): Target container is not valid.',
    );
    // 首次渲染,这里为 shouldReuseMarkup = false ,里面都是更新的逻辑
    if (shouldReuseMarkup) {
      ...
    }
    // 首次渲染,transaction.useCreateElement = true
    if (transaction.useCreateElement) {
      while (container.lastChild) {
        container.removeChild(container.lastChild);
      }
      DOMLazyTree.insertTreeBefore(container, markup, null);
    } else {
      setInnerHTML(container, markup);
      ReactDOMComponentTree.precacheNode(instance, container.firstChild);
    }
  },

The logic is simple: there are two ways to render markup into the DOM:

  • Clears the given container component, then inserts the markup into the given container.
  • Calls setInnerHTML to insert the markup into the given container and caches the virtual DOM onto the actual DOM node.

For the initial render, the first method is executed: clearing the container component and mounting the markup into the actual DOM. Call stack: DOMLazyTree.insertTreeBefore -> insertTreeChildren.

function insertTreeChildren(tree) {
  if (!enableLazy) {
    return;
  }
  var node = tree.node;
  var children = tree.children;
  if (children.length) {
    for (var i = 0; i < children.length; i++) {
      insertTreeBefore(node, children[i], null);
    }
  } else if (tree.html != null) {
    setInnerHTML(node, tree.html);
  } else if (tree.text != null) {
    setTextContent(node, tree.text);
  }
}

Recursively mounts all child components into the DOM.

Summary#

At this point, the entire logic for the initial render is complete. Overall, React and ReactDOM’s code is highly abstract, making it quite intricate to read. This article has focused solely on the overall process, without delving into every detail, as there are simply too many.

Finally, I’ve compiled a flowchart illustrating the process after ReactDOM.render executes, which can help in understanding the entire flow to some extent: The image is large and may not be clear when clicked and enlarged. It’s recommended to save it locally for a clearer view.

ReactDOM.render

Related Articles#

  • {% post_link react-source-code-analyze-1 React Source Code Analysis - Entry File %}
  • {% post_link react-source-code-analyze-2 React Source Code Analysis - ReactBaseClasses %}
  • {% post_link react-source-code-analyze-3 React Source Code Analysis - ReactChildren %}
  • {% post_link react-source-code-analyze-4 React Source Code Analysis - ReactElement %}
  • {% post_link react-source-code-analyze-5 React Source Code Analysis - onlyChildren %}

This article was published on September 21, 2017 and last updated on September 21, 2017, 2937 days ago. The content may be outdated.

How ReactDOM Renders Components to the DOM
https://blog.kisnows.com/en-US/2017/09/21/how-react-render-component-to-dom/
Author
Kisnows
Published at
2017-09-21
License
CC BY-NC-ND 4.0