In-depth understanding of React source code-Simple Component Rendering II (English)

In-depth understanding of React source code-Simple Component Rendering II (English)

Understanding The React Source Code Initial Rendering (Simple Component) II

Photo by Jez Timms on  Unsplash

In the last article we went through the process that creates a ReactCompositeComponentinstance from JSX expression. This one will continue the walk-through of simple component rendering, from the point of batchedMountComponentIntoNode().

Files used in this article:
renderers/dom/client/ReactMount.js : defines mountComponentIntoNode(), entry point of the logic process in this article, and ReactDOMContainerInfowhich is used in DOM rendering

renderers/shared/stack/reconciler/ReactReconciler.js : call mountComponentof variousReactXXXComponent

renderers/shared/stack/reconciler/ReactCompositeComponent.js : call mountComponentto instantiate TopLevelWrapper, and performInitialMountto instantiateReactDOMComponent

renderers/dom/shared/ReactDOMComponent.js : defineReactDOMComponent

A complete static call hierarchy used in this article:

|=ReactMount.render(nextElement, container, callback)     ___
|=ReactMount._renderSubtreeIntoContainer()                 |
  |-ReactMount._renderNewRootComponent()                   |
    |-instantiateReactComponent()                          |
    |~batchedMountComponentIntoNode()                  upper half
      |~mountComponentIntoNode()                 (platform agnostic)
        |-ReactReconciler.mountComponent()                 |
          |-ReactCompositeComponent.mountComponent()       |
          |-ReactCompositeComponent.performInitialMount()  |
            |-instantiateReactComponent()                 _|_
            |-ReactDOMComponent.mountComponent()       lower half
        |-_mountImageIntoNode()                  (HTML DOM specific)
                                                          _|_ 

batchedMountComponentIntoNode()does not do much. It simply invokes another function call to mountComponentIntoNode().

For now let's omit the delicacy of those indirect function calls and just see them as direct ones. I will cover transactionand batched updatesin later articles.

`mountComponentIntoNode()` the cross point of the two halves

mountComponentIntoNode()is the cross point of the platform agnostic code (in this article referred as upper half ) and HTML DOM specific one ( lower half ). All major tasks of this walk-through are also effectively completed within this function (and its sub-calls), from where 1) a ReactDOMComponentis derived from the top level ReactCompositeComponent[T]; 2) the ReactDOMComponentis rendered to a real DOM element, and 3) the elementis inserted into the documentobject.

function mountComponentIntoNode(
  wrapperInstance,  //scr: -----> ReactCompositeComponent[T]
  container,        //scr: -----> document.getElementById( root )
  transaction,      //scr: -----> not of interest
  shouldReuseMarkup,//scr: -----> null
  context,          //scr: -----> emptyObject
) { 
...
  var markup = ReactReconciler.mountComponent(//scr: -----> 1),2)
    wrapperInstance,
    transaction,
    null,
    ReactDOMContainerInfo(wrapperInstance, container),
    context,
    0/* parentDebugID */,
  ); 
... 
  ReactMount._mountImageIntoNode(             //scr: -----> 3)
    markup,
    container,
    wrapperInstance,
    shouldReuseMarkup,
    transaction,
  ); 
ReactMount@renderers/dom/client/ReactMount.js 

In which, 1) is still conducted as a high level operation in upper half, while 2), 3) are concrete DOM operations. After 2) completes, we will be able to see the element <h1 style={{ color : blue }}>hello world</h1>rendered on the screen.

1 the For), ReactReconciler.mountComponent()IS yet Another function that the Simple at The Calls in the Corresponding mountComponent()of internalInstancepassed to IT, in the this Case, ReactCompositeComponent[T].

mountComponent: function(
 internalInstance,
 transaction,
 hostParent,
 hostContainerInfo,
 context,
 parentDebugID,//0 in production and for roots
 ) {
  var markup = internalInstance.mountComponent(
    transaction,
    hostParent,
    hostContainerInfo,
    context,
    parentDebugID,
  );
...//scr: transaction related code
  return markup;
}, 
ReactReconciler@renderers/shared/stack/reconciler/ReactReconciler.js 

One special parameter here is ReactDOMContainerInfo, it is constructed at the same time when it is passed down to ReactReconciler.mountComponent():

function ReactDOMContainerInfo(topLevelWrapper, node) {
  var info = {
    _topLevelWrapper: topLevelWrapper,//scr: -------------------> ReactCompositeComponent[T]
    _idCounter: 1,
    _ownerDocument: node ? node.nodeType === DOC_NODE_TYPE ? node : node.ownerDocument : null,//scr: -----> node.nowerDocument
    _node: node,//src: -----> document.getElementById( root )
    _tag: node ? node.nodeName.toLowerCase() : null,//scr: -----> 'div'
    _namespaceURI: node ? node.namespaceURI : null  //scr: ----->
element.namespaceURI
  }; 
...//scr: DEV code
  return info;
} 
ReactDOMContainerInfo@renderers/dom/client/ReactMount.js 

The result object of this constructor is ReactDOMContainerInfo[ins]which will be used by 3).

Here is the end of the corridor that consists of transient functions. Now we move on to the next important stop.

`ReactCompositeComponent.mountComponent()` initialize `ReactCompositeComponent[T]`

This is where the magic, I mean, the reaction happens

In the previous step only _currentElementof ReactCompositeComponent[T]is populated with a reference to ReactElement[2], which makes the object a little bit dull. But not anymore. This property will in turn be used to trigger a reaction within ReactCompositeComponent[T]and transforms (initialize) the object to something more meaningful.

The designated data structure of this step is:

The call stack in action is:

ReactDOM.render
|=ReactMount.render(nextElement, container, callback)
|=ReactMount._renderSubtreeIntoContainer()
  |-ReactMount._renderNewRootComponent()
  |-instantiateReactComponent()
    |~batchedMountComponentIntoNode(
        componentInstance,//scr: -----> ReactCompositeComponent[T]
        container,       //scr: -> document.getElementById( root )
        shouldReuseMarkup,//scr: -----> null
        context,          //scr: -----> emptyObject
      )
      |~mountComponentIntoNode(
          wrapperInstance,//scr: -----> ReactCompositeComponent[T]
          container,      //scr: -----> same
          transaction,    //scr: -----> not of interest
          shouldReuseMarkup,//scr: ---> same
          context,        //scr: -----> not of interest
        )
        |-ReactReconciler.mountComponent(
            internalInstance,//scr: --> ReactCompositeComponent[T]
            transaction,     //scr: --> not of interest
            hostParent,      //scr: --> null
            hostContainerInfo,//scr: --> ReactDOMContainerInfo[ins]
            context,         //scr: --> not of interest
            parentDebugID,   //scr: --> 0
          )
         /* we are here */      
          |-ReactCompositeComponent[T].mountComponent(same) 

Next let's look at the ReactCompositeComponent.mountComponent()implementation.

I will not show the implementation for small functions (when they are get called) but give return value directly for the sake of concision.
mountComponent: function(
 transaction,
 hostParent,
 hostContainerInfo,
 context,
//scr: this ------> ReactCompositeComponent[T]
 ) { 
//scr: --------------------------------------------------------> 1)
  this._context = context;         //scr: -----> emptyObject
  this._mountOrder = nextMountID++;//scr: ----------------------> global veriable, accumulative
  this._hostParent = hostParent;   //scr: -----> null
  this._hostContainerInfo = hostContainerInfo;//scr: -----------> ReactDOMContainerInfo[ins] 
  var publicProps = this._currentElement.props;//scr: ----------> { child: ReactElement[1] } 
  var publicContext = this._processContext(context);//scr: -----> meaning less, emptyObject 
//scr: --------------------------------------------------------> 2)
  var Component = this._currentElement.type;//scr: -------------> TopLevelWrapper 
  var updateQueue = transaction.getUpdateQueue();//scr: --------> not of interest 
//Initialize the public class
  var doConstruct = shouldConstruct(Component);//scr: ----------> true, for TopLevelWrapper.prototype.isReactComponent = {}; 
  var inst = this._constructComponent(
    doConstruct,
    publicProps,
    publicContext,
    updateQueue,
  );//scr: ----------> call TopLevelWrapper s constructor 
  var renderedElement; 
//Support functional components
  if (!doConstruct && (inst == null || inst.render == null)) {
 
  } else {
    if (isPureComponent(Component)) {//scr: --------------------> TopLevelWrapper.prototype.isPureReactComponent is not defined
 
    } else {
      this._compositeType = CompositeTypes.ImpureClass;
    }
  } 
//scr: --------------------------------------------------------> 3)
//These should be set up in the constructor, but as a convenience   
//for simpler class abstractions, we set them up after the fact.
  inst.props = publicProps;//scr: ----> { child: ReactElement[1] }
  
//scr: --------------------------------------------------------> 4)
  this._instance = inst;//scr: ---------------------------------> link the ReactCompositeComponent[T] to the TopLevelWrapper instance 
//Store a reference from the instance back to the internal representation
  ReactInstanceMap.set(inst, this);//scr: ----------------------> link the TopLevelWrapper instance back to ReactCompositeComponent[T] 
  
  var markup;
  if (inst.unstable_handleError) {//scr: -----------------------> false, TopLevelWrapper.prototype.unstable_handleError is not defined
 
  } else { 
//scr: --------------------------------------------------------> 5)
    markup = this.performInitialMount(//scr: a initial at the end?
      renderedElement,
      hostParent,
      hostContainerInfo,
      transaction,
      context,
    );
  } 
  
  return markup;
} 
ReactCompositeComponent@renderers/shared/stack/reconciler/ReactCompositeComponent.js 

Running in the context of ReactCompositeComponent[T](as a member function), this method

1) assigns the parameters directly to the corresponding properties of ReactCompositeComponent[T]and local variables. The interesting one variable here is publicProps, I will its usage very soon;

2) extracts TopLevelWrapperfrom this._currentElement.type, and calls its constructor to create a TopLevelWrapperinstance. In this process:

shouldConstruct(Component) 

checks if TopLevelWrapper.prototype.isReactComponentis set, and returns trueif so; and

this._constructComponent() 

calls the TopLevelWrapperconstructor if trueis the result of the previous function. I will name the instance TopLevelWrapper[ins]this time;

This is the time you may want to check the definition of TopLevelWrapperin the previous post , search for ***.

3) initializes the TopLevelWrapper[ins].propsof the new instance with the information stored previously in the wrapper element ReactElement[2]through the publicPropsin 1);

4) creates a doubly link between this( ReactCompositeComponent[T]) and TopLevelWrapper[ins]. One link is created using this._instance, another is made with ReactInstanceMap. this._instance Will be used very soon in the next step, and ReactInstanceMapwill be used in later posts;

I type 4 stars **** here as you might need to come back to check ReactInstanceMap's origin.

5) goes to the next step.

`ReactCompositeComponent.performInitialMount()` create a `ReactDOMComponent` from `ReactElement[1]`

This step strips the wrapper and creates a ReactDOMComponentinstance. Plus, we are going to see ReactHostComponentthe first time. This component is used to chain the function call from the upper half to lower half.

First thing first, the target data structure:

The call stack in action:

ReactDOM.render
|=ReactMount.render(nextElement, container, callback)
|=ReactMount._renderSubtreeIntoContainer()
  |-ReactMount._renderNewRootComponent()
    |-instantiateReactComponent()
    |~batchedMountComponentIntoNode()
      |~mountComponentIntoNode()
        |-ReactReconciler.mountComponent()
          |-ReactCompositeComponent[T].mountComponent(same)
           /* we are here */
            |-ReactCompositeComponent[T].performInitialMount(
                renderedElement,  //scr: -------> undefined
                hostParent,       //scr: -------> null
                hostContainerInfo,//scr: -------> ReactDOMContainerInfo[ins]
                transaction,      //scr: -------> not of interest
                context,          //scr: -------> not of interest
              ) 

ReactCompositeComponent.performInitialMount()does three things. It 1) extracts the ReactElement[1]as mentioned before; 2) instantiates a ReactDOMComponentbased on the ReactElement[1].type; and 3) calls ReactDOMComponent.mountComponent()to render a DOM element.

why a performInitialMountis invoked at the end of the mountComponent function? It is initial for the nextmountComponent
performInitialMount: function(
 renderedElement,
 hostParent,
 hostContainerInfo,
 transaction,
 context,
) {
  var inst = this._instance; 
  var debugID = 0; 
  if (inst.componentWillMount) {//scr: ----------> undefined
 
  } 
 //scr: ------------------------------------------------------> 1)
 //If not a stateless component, we now render
  if (renderedElement === undefined) {
    renderedElement = this._renderValidatedComponent();//scr: ---> calls TopLevelWrapper.render() to extract ReactElement[1].
  } 
 //scr: ------------------------------------------------------> 2)
  var nodeType = ReactNodeTypes.getType(renderedElement);//scr: -> ReactNodeTypes.HOST 
  this._renderedNodeType = nodeType;
  var child = this._instantiateReactComponent(
    renderedElement,
    nodeType !== ReactNodeTypes.EMPTY/* shouldHaveDebugID */,
  ); 
  this._renderedComponent = child; 
 //scr: ------------------------------------------------------> 3)
  var markup = ReactReconciler.mountComponent(
    child,
    transaction,
    hostParent,
    hostContainerInfo,
    this._processChildContext(context),
    debugID,
  ); 
  return markup;
}, 
ReactCompositeComponent@renderers/shared/stack/reconciler/ReactCompositeComponent.js 

Now I give a detailed explanation for each step:

1) this._renderValidatedComponent()simply calls TopLevelWrapper.render()so that ReactElement[1]extracted from TopLevelWrapper[T].props.childis assigned to renderedElement.

2) this._instantiateReactComponent()is an alias to that has been discussed in the last post. However, is used to create a instance this time _instantiateReactComponent@renderers/shared/stack/reconciler/instantiateReactComponent.jsrenderedElement(i.e., ReactElement[1])ReactDOMComponent

wait here,

if we look at the implementation of _instantiateReactComponent()again

...
//Special case string values
 if (typeof element.type ===  string ) {//scr: -------> this time 
   instance = ReactHostComponent.createInternalComponent(element);
 }
... 
_instantiateReactComponent@renderers/shared/stack/reconciler/instantiateReactComponent.js 

the createInternalComponent()of ReactHostComponentis got called because ReactElement[1].typeis h1 , which instantiate a genericComponentClass:

function createInternalComponent(element) {
...
  return new genericComponentClass(element);
} 
ReactHostComponent@renderers/shared/stack/reconciler/ReactHostComponent.js 

but why it has anything to do with ReactDOMComponent?

In fact, the platform specific component ReactDOMComponentis injected to ReactHostComponentas genericComponentClassduring compiling time. For now we consider link has already been established and injection mechanism will be discussed later posts.

I type *5 here.

The constructor of ReactDOMComponentis very similar to that of ReactCompositeComponent:

function ReactDOMComponent(element) {
  var tag = element.type;        //scr: --------> 'h1'
...
  this._currentElement = element;//scr: --------> ReactElement[1]
  this._tag = tag.toLowerCase(); //scr: --------> 'h1'
...//scr: default values, null, 0, etc.
} 
ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js 

We name the returned instance ReactDOMComponent[ins];

3) ReactReconciler.mountComponent()has already been covered, it simply calls the mountComponent()of the first parameter, in this case,. ReactDOMComponent[ins]Now the logic process goes to lower half.

to be continued...