Understanding the React Source Code — Initial Rendering (Simple Component) I

Photo by Gerrie van der Walt
on Unsplash

UI updating, in its essential, is data change. React offers a straightforward and intuitive way to program front-end Apps as all the moving parts are converged in the form of states. Code review of Apps made with React is a bit more easier to me as I like to start with data structures for a rough expectation of the functionalities and processing logic. From time to time, I was curious about how React works internally, hence this article. Moreover, I think it never hurts to have a deeper understanding down the stack, as it gives me more freedom when I need a new feature, more confidence when I want to contribute and more comfort when I upgrade.

This article will start walking through one of the critical paths of React by rendering a simple component, i.e., a


. Other topics (e.g., Composite components rendering, state driven UI updating and components life cycle) will be discussed in a similar actionable manner in the following articles.

Files used in this article:

isomorphic/React.js
: entry point of ReactElement.createElement()

isomorphic/classic/element/ReactElement.js
: workhorse of ReactElement.createElement()

renderers/dom/ReactDOM.js
: entry point of ReactDOM.render()

renderers/dom/client/ReactMount.js
: workhorse of ReactDom.render()

renderers/shared/stack/reconciler/instantiateReactComponent.js: create different types of ReactComponents
based on element type

renderers/shared/stack/reconciler/ReactCompositeComponent.js: ReactComponents
wrapper of root element

Tags used in the call stack:

-
function call

=
alias

~
indirect function call

As the locations of source code files can not be obviously derived from import
statement in the flat module tree
, I will use @
to help locating the code snippet in demonstration.

Last words, this series is based on React 15.6.2.

From JSX to `React.createElement()`

After using the function intensively in a React project for a couple of months, I did not even know the existence of this React.createElement()
because it is masked by JSX from a developer’s point of view.

In compiling time, components defined in JSX is translated by Babel to React.createElement()
called with appropriate parameters. For instance, the default App.js shipped with create-react-app
:

import React, { Component } from ‘react’;
import logo from ‘./logo.svg’;
import ‘./App.css’;
class App extends Component {
  render() {
    return (
      
”logo”

Welcome to React

To get started, edit src/App.js and save to reload.

); } }
export default App;

is compiled to:

import React, { Component } from ‘react’;
import logo from ‘./logo.svg’;
import ‘./App.css’;
class App extends Component {
  render() {
    return React.createElement(
      ‘div’,
      { className: ‘App’ },
      React.createElement(
        ‘header’,
        { className: ‘App-header’ },
        React.createElement(‘img’, { src: logo, className: ‘App-logo’, alt: ‘logo’ }),
        React.createElement(
          ‘h1’,
          { className: ‘App-title’ },
          ‘Welcome to React’
        )
      ),
      React.createElement(
        ‘p’,
        { className: ‘App-intro’ },
        ‘To get started, edit ‘,
        React.createElement(
          ‘code’,
          null,
          ‘src/App.js’
        ),
        ‘ and save to reload.’
      )
    );
  }
}
export default App;

which is the real code executed by a browser. The code above shows a definition of a composite component App
, in which JSX, a syntax of interweaving HTML tag in JavaScript code (e.g.,


), is translated to React.createElement()
calls.

This component will be rendered like this:

ReactDOM.render(
  ,
  document.getElementById(‘root’)
);

normally by a file named as “index.js”.

This nested components tree is a bit too complicated to be an ideal start point, so let’s forget about the code above for now look at something simpler – that renders a simple HTML element.

…
ReactDOM.render(
  

hello world

, document.getElementById(‘root’) ); …

babeled version

…
ReactDOM.render(React.createElement(
  ‘h1’,
  { style: { “color”: “blue” } },
  ‘hello world’
), document.getElementById(‘root’));
…

`React.createElement()` — creating a `ReactElement`

The first step does not do much really. It simply constructs an ReactElement
instance populated with whatever passed down to the call stack. The data structure is as following:

The call stack to achieve the purpose of this step is as following:

React.createElement
|=ReactElement.createElement(type, config, children)
   |-ReactElement(type,…, props)

1. React.createElement(type, config, children)
is merely an alias of ReactElement.createElement()
;

…
var createElement = ReactElement.createElement;
…
var React = {
…
  createElement: createElement,
…
};
module.exports = React;
[email protected]/React.js

2. ReactElement.createElement(type, config, children)
1) copies the elements in config
to props
, 2) copies the children
to props.children
and 3) copies the type.defaultProps
to props
;

…
  // 1)
  if (config != null) {
    …extracting not interesting properties from config…
    // Remaining properties are added to a new props object
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }
// 2)
  // Children can be more than one argument, and those are transferred onto
  // the newly allocated props object.
  var childrenLength = arguments.length — 2;
  if (childrenLength === 1) {
    props.children = children; // scr: one child is stored as object
  } else if (childrenLength > 1) {
    var childArray = Array(childrenLength);
    for (var i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2]; // scr: multiple children are stored as array
    }
props.children = childArray;
  }
// 3)
  // Resolve default props
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
…
[email protected]/classic/element/ReactElement.js

3. Then ReactElement(type,…, props)
copies the type
and props
as they are to ReactElement
and returns the instance.

…
var ReactElement = function(type, key, ref, self, source, owner, props) {
  // This tag allow us to uniquely identify this as a React Element
  $$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
  type: // scr: --------------> ‘h1’
  key:  // scr: --------------> not of interest for now
  ref:  // scr: --------------> not of interest for now
  props: {
    children:     // scr: --------------> ‘hello world’
    …other props: // scr: --------------> style: { “color”: “blue” }
  },
// Record the component responsible for creating this element.
  _owner:  // scr: --------------> null
};
…
[email protected]/classic/element/ReactElement.js

The fields populated in the newly constructed ReactElement
will be used directly by ReactMount.instantiateReactComponent()
, which will be explained in detail very soon. Note that the next step will also create a ReactElement
object with ReactElement.createElement()
, so I will call the ReactElement
object of this phase ReactElement[1]
.

`ReactDom.render()` — and render it

`_renderSubtreeIntoContainer()` — attach `TopLevelWrapper` to the `ReactElement[1]`

The purpose of the next step is to wrap the ReactElement[1]
with another ReactElement
(we call the instance a [2]
) and set the ReactElement.type
with TopLevelWrapper
. The name TopLevelWrapper
explains what it does — wrap the top level element (of the DOM hierarchy passed through render()
):

An important definition here is that of TopLevelWrapper
:

…
var TopLevelWrapper = function() {
  this.rootID = topLevelRootCounter++;
};
TopLevelWrapper.prototype.isReactComponent = {};
TopLevelWrapper.prototype.render = function() { 
// scr: this function will be used to strip the wrapper later in the // rendering process
return this.props.child;
};
TopLevelWrapper.isReactTopLevelWrapper = true;
…
[email protected]/dom/client/ReactMount.js

Please note that the entity assigned to ReactElement.type
is a type ( TopLevelWrapper
) which will be instantiated in the following rendering steps. Then ReactElement[1]
will be extracted from this.props.child
.

The call stack to construct the designated object is as following:

ReactDOM.render
|=ReactMount.render(nextElement, container, callback)
|=ReactMount._renderSubtreeIntoContainer(
   parentComponent, // scr: --------------> null
   nextElement,     // scr: -------------->  ReactElement[1]
   container,// scr: --------------> document.getElementById(‘root’)
   callback’ // scr: --------------> undefined
)

For initial rendering, ReactMount._renderSubtreeIntoContainer()
is simpler than it seems to be, in fact, most branches (for UI updating) in this function are skipped. The only line that is effective before the logic processes to next step is

…
  var nextWrappedElement = React.createElement(TopLevelWrapper, {
    child: nextElement,
  });
…
[email protected]/dom/client/ReactMount.js

Now that it should be easy to see how the target object of this step is constructed I will not repeat React.createElement
.

`instantiateReactComponent()` — create a `ReactCompositeComponent` using `ReactElement[2]`

This is step is to create an primitive ReactCompositeComponent
for the top level component:

The call stack of this step is:

ReactDOM.render
|=ReactMount.render(nextElement, container, callback)
|=ReactMount._renderSubtreeIntoContainer()
  |-ReactMount._renderNewRootComponent(
      nextWrappedElement, // scr: ------> ReactElement[2]
      container, // scr: ------> document.getElementById(‘root’)
      shouldReuseMarkup, // scr: null from ReactDom.render()
      nextContext, // scr: emptyObject from ReactDom.render()
    )
    |-instantiateReactComponent(
        node, // scr: ------> ReactElement[2]
        shouldHaveDebugID /* false */
      )
      |-ReactCompositeComponentWrapper(
          element // scr: ------> ReactElement[2]
      );
      |=ReactCompositeComponent.construct(element)

instantiateReactComponent
is the only function that is long enough to discuss here. In our context, it check the ReactElement[2]
’s type
(i.e., TopLevelWrapper
) and create a ReactCompositeComponent
accordingly.

function instantiateReactComponent(node, shouldHaveDebugID) {
  var instance;
…
  } else if (typeof node === ‘object’) {
    var element = node;
    var type = element.type;
…
// Special case string values
    if (typeof element.type === ‘string’) {
…
    } else if (isInternalComponentType(element.type)) {
…
    } else {
      instance = new ReactCompositeComponentWrapper(element);
    }
  } else if (typeof node === ‘string’ || typeof node === ‘number’) {
…
  } else {
…
  }
…
  return instance;
}
[email protected]/shared/stack/reconciler/instantiateReactComponent.js

One thing worth noting here is that new ReactCompositeComponentWrapper()
is a direct call of ReactCompositeComponent
constructor:

…
// To avoid a cyclic dependency, we create the final class in this module
var ReactCompositeComponentWrapper = function(element) {
  this.construct(element);
};
…
…
Object.assign(
  ReactCompositeComponentWrapper.prototype,
  ReactCompositeComponent,
  {
    _instantiateReactComponent: instantiateReactComponent,
  },
);
…
[email protected]/shared/stack/reconciler/instantiateReactComponent.js

Then the real constructor get called:

construct: function(element /* scr: ------> ReactElement[2] */) {
  this._currentElement = element;
  this._rootNodeID = 0;
  this._compositeType = null;
  this._instance = null;
  this._hostParent = null;
  this._hostContainerInfo = null;
// See ReactUpdateQueue
  this._updateBatchNumber = null;
  this._pendingElement = null;
  this._pendingStateQueue = null;
  this._pendingReplaceState = false;
  this._pendingForceUpdate = false;
this._renderedNodeType = null;
  this._renderedComponent = null;
  this._context = null;
  this._mountOrder = 0;
  this._topLevelWrapper = null;
// See ReactUpdates and ReactUpdateQueue.
  this._pendingCallbacks = null;
// ComponentWillUnmount shall only be called once
  this._calledComponentWillUnmount = false;
},
[email protected]/shared/stack/reconciler/ReactCompositeComponent.js

The following steps will also create ReactCompositeComponent
objects with instantiateReactComponent()
, so I will call the object of this phase ReactCompositeComponent[T]
(T for top).

After the ReactCompositeComponent
object is constructed, the next step is to call batchedMountComponentIntoNode
and initialize the component instance and mount it, which will be discussed in detail in the next article.

Hacker Noon稿源:Hacker Noon (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 移动开发 » Understanding the React Source Code — Initial Rendering (Simple Component) I

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录