Understanding The React Source Code — UI Updating (Individual DOM) VIII

Holmes He
source code
Published in
13 min readFeb 13, 2018

Understanding The React Source Code I
Understanding The React Source Code II
Understanding The React Source Code III
Understanding The React Source Code IV
Understanding The React Source Code V
Understanding The React Source Code VI
Understanding The React Source Code VII
Understanding The React Source Code VIII (this one)
Understanding The React Source Code IX

UI updating, in its essential, is data change. React offers a straightforward and intuitive way to program front-end Apps as most moving parts are converged in the form of states, and most UI tasks can be done with a single

…, I mean, a single method, setState(). In this article, we are going unfold the setState() implementation, and peek inside the diffing algorithm by mutating an individual DOM element.

Before we get started, I would like to respond to one common feedback from readers: “why 15.x, why not fiber?”

Well, simply put, because synchronous rendering is still alive. Thus, the code base (a.k.a., stack reconciler) specifically designed for synchronous rendering, in my opinion, offers an easier albeit more solid ground to establish an initial understanding.

Firstly let’s extend an example from {post four}

class App extends Component {
constructor(props) {
super(props);
this.state = {
desc: 'start',
color: 'blue'
};
this.timer = setTimeout(
() => this.tick(),
5000
);
}
tick() {
this.setState({
desc: 'end',
color: 'green'
});
}
render() {
return (
<div className="App">
<div className="App-header">
<img src="main.jpg" className="App-logo" alt="logo" />
<h1> "Welcom to React" </h1>
</div>
<p className="App-intro" style={{color: this.state.color}}>
{ this.state.desc }
</p>
</div>
);
}
}
export default App;

Compared to the App component used in {post four}, the new version adds style prop to <p> node, and setState()s desc to 'end' and color to 'green' 5 seconds after the component is constructed.

The instantiating of App has been discussed in {post four}.

ctl-f “setState”

In the same article, I also mentioned ReactInstanceMap, a back link (from the external ReactComponent instance) to the internal ReactCompositeComponent[ins], which will be used very soon.

Here I paste the data structure as a reminder.

Figure-I

Before transactions

We start from the setState() method body:

ReactComponent.prototype.setState = function (
partialState,
callback
) {
// scr: ---> sanity check
this.updater.enqueueSetState(this, partialState);
if (callback) {
// scr: ---> no callbak
}
};
ReactComponent@isomorphic/modern/class/ReactBaseClasses.js

Yes, setState() is inherited from ReactComponent.

But wait, what is this.updater? isn’t it set to ReactNoopUpdateQueue in the constructor, and is a no-op? In fact, I believe with the understanding of Transaction(s) and instance pooling {last post}, if you trace back from the aforementioned ReactComponent instantiating {post four}, you will be able to find out the origin of this.updater very easily.

I will leave this question open so we can move faster to the core part —virtual DOM and diffing algorithm

enqueueSetState: function (publicInstance, partialState) {
// scr: DEV code
// scr: ------------------------------------------------------> 1)
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
if (!internalInstance) {
return;
}
// scr: ------------------------------------------------------> 2)
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
// scr: ------------------------------------------------------> 3)
enqueueUpdate(internalInstance);
},
ReactUpdateQueue@renderers/shared/stack/reconciler/ReactUpdateQueue.js

1) this is the method that obtains the internal ReactCompositeComponent[ins] from the back link ReactInstanceMap;

function getInternalInstanceReadyForUpdate(
publicInstance,
callerName
) {
var internalInstance = ReactInstanceMap.get(publicInstance);
... // scr: DEV code return internalInstance;
}
getInternalInstanceReadyForUpdate@renderers/shared/stack/reconciler/ReactUpdateQueue.js

2) attach an array (_pendingStateQueue) to ReactCompositeComponent[ins], and push the changed state {desc:'end',color:'green'} into it;

3) start the Transaction(s) {post six, seven},

...
function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
...
enqueueUpdate@renderers/shared/stack/reconciler/ReactUpdateQueue.js

The call stack so far:

|-ReactComponent.setState()
|-ReactUpdateQueue.enqueueSetState()
|-getInternalInstanceReadyForUpdate()
|-enqueueUpdate()
|-ReactUpdates.enqueueUpdate()
|~~~

Here I also paste the transaction related call graph as a reminder.

Figure-II

In transactions

The first stop after the Transaction(s) are fully initialized is

function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
// scr: -----------------------------------> sanity check
...
dirtyComponents.sort(mountOrderComparator); updateBatchNumber++; for (var i = 0; i < len; i++) {
var component = dirtyComponents[i];
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;
// scr: ------------------------------> logging
...
ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction, updateBatchNumber);

// scr: ------------------------------> logging
if (callbacks) { // scr: -------------> no callbacks
...
}
}
}
ReactUpdates@renderers/shared/stack/reconciler/ReactUpdates.js

This time we have one dirtyComponents, ReactCompositeComponent[ins] which is the first parameter of ReactReconciler.performUpdateIfNecessary().

performUpdateIfNecessary: function (
internalInstance,
transaction,
updateBatchNumber
) {
// scr: DEV code
...
internalInstance.performUpdateIfNecessary(transaction);// scr: DEV code
...
}
ReactReconciler@renderers/shared/stack/reconciler/ReactUpdates.js

Like most of the other methods in ReactReconciler class, ReactReconciler.performUpdateIfNecessary() will call the component’s same method, ReactCompositeComponent.performUpdateIfNecessary()

it’s like a polymorphism in a more explicit way

performUpdateIfNecessary: function (transaction) {
if (this._pendingElement != null) {
// scr: -----------> condition not applied
...
} else if (
this._pendingStateQueue !== null ||
this._pendingForceUpdate
) {
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
} else {
// scr: -----------> condition not applied
...
}
},
ReactCompositeComponent@renderers/shared/stack/reconciler/ReactCompositeComponent.js

It in turn calls ReactCompositeComponent[ins].updateComponent(). Note that _pendingStateQueue is set right before the logic enters the Transaction context.

updateComponent: function(
transaction,
prevParentElement,
nextParentElement,
prevUnmaskedContext,
nextUnmaskedContext,
) {
var inst = this._instance; // scr: ---------------------------> 1)
// scr: sanity check and code that is not applicable this time
...
// scr: ------------------------------------------------------> 2)
var nextState = this._processPendingState(nextProps, nextContext);
var shouldUpdate = true; if (!this._pendingForceUpdate) {
if (inst.shouldComponentUpdate) { // scr: ------------------> 3)
shouldUpdate = inst.shouldComponentUpdate(
nextProps,
nextState,
nextContext,
);
} else {
if (this._compositeType === CompositeTypes.PureClass) {
// scr: ---------------> it is ImpureClass, not applicable
...
}
}
}
this._updateBatchNumber = null;
if (shouldUpdate) {
this._pendingForceUpdate = false;
// Will set `this.props`, `this.state` and `this.context`.
this._performComponentUpdate( // scr: --------------------> 4)
nextParentElement,
nextProps,
nextState,
nextContext,
transaction,
nextUnmaskedContext,
);
} else {
// scr: code that is not applicable this time
...
}
},
ReactCompositeComponent@renderers/shared/stack/reconciler/ReactCompositeComponent.js

1) obtain the external ReactComponent instance (App) from ReactCompositeComponent[ins]._instance {Figure-I};

2) merge the partial state in ReactCompositeComponent[ins]._pendingStateQueue ({desc:'end',color:'green'}) and existing states using Object.assign();

_processPendingState: function(props, context) {
// scr: -------> obtain the App {Figure-I}
var inst = this._instance;
var queue = this._pendingStateQueue;
// scr: code that is not applicable this time
...
var nextState =
Object.assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
var partial = queue[i];
Object.assign(
nextState,
typeof partial === 'function'
? partial.call(inst, nextState, props, context)
: partial,
);
}
return nextState;
},
ReactCompositeComponent@renderers/shared/stack/reconciler/ReactCompositeComponent.js

3) this is the lifecycle function that is provided to the developers to avoid reconciliation (the following processing logic) from being executed in case setState() does not change the critical states;

Most likely you do not need this function

4) enter the next stop.

_performComponentUpdate: function(
nextElement,
nextProps,
nextState,
nextContext,
transaction,
unmaskedContext,
) {
var inst = this._instance; // scr: {Figure-I}
// scr: code that is not applicable this time
...
// scr: invoke App's life cycle method if defined
if (inst.componentWillUpdate) {
inst.componentWillUpdate(nextProps, nextState, nextContext);
}
// scr: code that is not applicable this time
...
inst.state = nextState;
...
this._updateRenderedComponent(transaction, unmaskedContext); // scr: queue App's life cycle method if defined
if (hasComponentDidUpdate) {
...
}
},
ReactCompositeComponent@renderers/shared/stack/reconciler/ReactCompositeComponent.js

It simply sets the App’s state to the newly merged one. And calls this._updateRenderedComponent() which is the entry point of the diffing algorithm.

The call stack so far,

...
|~~~
|-runBatchedUpdates()
|-performUpdateIfNecessary()
|-ReactCompositeComponent[ins].performUpdateIfNecessary()
|-this.updateComponent()
|-this._processPendingState()
|-this._performComponentUpdate() ___
|-this._updateRenderedComponent() |
... diffing

Then the logic processes to the diffing algorithm.

Virtual DOM

Before we start examining the Diffing algorithm, we better have a consent about what exactly are virtual DOMs, as the term did not appear in the code base.

Here I paste an image from {post five} as a reminder:

Figure-III

The ReactElements are the virtual DOMs we are going to agree on. {post five} also discussed how the virtual DOM tree are initially established.

In MVC terms ReactElements are modals which contain only data. On the other hand ReactDOMComponents are controllers that offer actionable methods.

Diffing

The figure above gives the old virtual DOM tree that is generated in {post four, five}.

ctl-f “in _renderValidatedComponent()”

This step will generate a new one with ReactCompositeComponent[ins]._renderValidatedComponent() based on the changed states, for the purpose of diffing.

_updateRenderedComponent: function (transaction, context) {
var prevComponentInstance = this._renderedComponent; // scr: -> 1)
// scr: ------------------------------------------------------> 2)
var prevRenderedElement = prevComponentInstance._currentElement;
// scr: create a new DOM tree
var nextRenderedElement = this._renderValidatedComponent();
var debugID = 0; // scr: DEV code
...
if (shouldUpdateReactComponent( // scr: ----------------------> 3)
prevRenderedElement,
nextRenderedElement)
) {
ReactReconciler.receiveComponent( // scr: ------------------> 5)
prevComponentInstance,
nextRenderedElement,
transaction,
this._processChildContext(context)
);
} else { // scr: ---------------------------------------------> 4)
// scr: code that is not applicable this time
...
}
},
ReactCompositeComponent@renderers/shared/stack/reconciler/ReactCompositeComponent.js

1) obtain ReactDOMComponent[6] through ReactCompositeComponent[ins] {Figure-I};

2) cascading call of React.createElement() in App[ins].render() to create the new virtual DOM tree {post four}, in which the only different DOM node is:

3) the first comparison of diffing algorithm is between types of the old and new root elements;

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;
}
}
shouldUpdateReactComponent@renderers/shared/shared/shouldUpdateReactComponent.js

4) if they are not the same, build the new tree from scratch — the component mounting process is similar to that discussed in {post five};

whenever the root elements have different types, React will tear down the old tree and build the new tree from scratch

5) if the a the same so, start the DOM updating process.

receiveComponent: function (nextElement, transaction, context) {
var prevElement = this._currentElement;
this._currentElement = nextElement;
this.updateComponent(transaction,
prevElement,
nextElement,
context);
},
updateComponent: function(
transaction,
prevElement,
nextElement,
context
) {
var lastProps = prevElement.props;
var nextProps = this._currentElement.props;
// scr: code that is not applicable this time
...
// scr: ------------------------------------------------------> 1)
this._updateDOMProperties(lastProps, nextProps, transaction);
// scr: ------------------------------------------------------> 2)
this._updateDOMChildren(lastProps, nextProps, transaction, context);
// scr: code that is not applicable this time
...
},
ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js

1) get the props from the old virtual DOM (lastProps) and the newly created one (nextProps);

2)ReactDOMComponent._updateDOMProperties() checks the old and new versions of a DOM’s props, and calls CSSPropertyOperations.setValueForStyles() to update the DOM if different;

3) ReactDOMComponent._updateDOMChildren() checks the old and new versions of a DOM’s content (text, inner HTML), and calls ReactDOMComponent.updateTextContent() to update the DOM’s (text) content if different.

The static call stack:

...                                                            ___
ReactReconciler.receiveComponent() <----------------| |
|-ReactDOMComponent.receiveComponent() | |
|-this.updateComponent() | |
|-this._updateDOMProperties() | diffing
|-CSSPropertyOperations.setValueForStyles() | |
|-this._updateDOMChildren() | |
|-this.updateTextContent() | |
|-recursing children (not the focus this time) --| |
---

By observing the static call stack, it is not hard to deduce the how the recursion works.

1) one iteration of this recursion updates the properties of one virtual DOM;

2) ReactDOMComponent.updateDOMChildren() is also responsible to go through the current virtual DOM’s direct children and invoke the next iteration for each of them.

note that sub DOM recursing is not the focus of this post

I collapse some method calls in the above call stack,

|-ReactReconciler.receiveComponent()
|-ReactDOMComponent[n].receiveComponent()
|-this.updateComponent()
=>|-ReactDOMComponent[n].updateComponent()

and draw the call stack in action for clarity:

...
|-ReactDOMComponent[6].updateComponent()
|-this._updateDOMProperties() // scr: ----> same
|-this._updateDOMChildren
|-recursing children (not the focus this time...)
|-ReactDOMComponent[4].updateComponent()
|-this._updateDOMProperties() // scr: ----> same
|-this._updateDOMChildren
|-recursing children (not the focus this time...)
|-ReactDOMComponent[2].updateComponent()
|-this._updateDOMProperties() // scr: ----> same
|-this._updateDOMChildren // scr: ----> same
|-ReactDOMComponent[3].updateComponent()
|-this._updateDOMProperties() // scr: ----> same
|-this._updateDOMChildren // scr: ----> same
|-ReactDOMComponent[5].updateComponent()
|-this._updateDOMProperties()
|-CSSPropertyOperations.setValueForStyles()
|-this._updateDOMChildren
|-this.updateTextContent()

`ReactDOMComponent._updateDOMProperties()` —check if a DOM changed

This is the overlooked method in {post three *6}

In this article we focus on only STYLE updating related code.

_updateDOMProperties: function(lastProps, nextProps, transaction) {
var propKey;
var styleName;
var styleUpdates;
// scr: --------------------------------------------------------> 1)
for (propKey in lastProps) {
if (
nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey) ||
lastProps[propKey] == null
) {
continue;
}
if (propKey === STYLE) {
var lastStyle = this._previousStyleCopy;
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = '';
}
}
this._previousStyleCopy = null;
} else if ... {
// scr: not the focus this time
...
}
}
// scr: ----------------------------------------------------> end 1)
for (propKey in nextProps) {
var nextProp = nextProps[propKey];
var lastProp = propKey === STYLE
? this._previousStyleCopy
: lastProps != null ? lastProps[propKey] : undefined;
if (
!nextProps.hasOwnProperty(propKey) ||
nextProp === lastProp ||
(nextProp == null && lastProp == null)
) {
continue;
}
if (propKey === STYLE) {
if (nextProp) {
// scr: DEV code
...
// scr: -------------------------------------------------> 2)
nextProp = this._previousStyleCopy = Object.assign({}, nextProp);
} else {
this._previousStyleCopy = null;
}
if (lastProp) { // scr: ----------------------------------> 3)
// scr: the comment applies here -----------------------> a)
// Unset styles on `lastProp` but not on `nextProp`.

for (styleName in lastProp) {
if (
lastProp.hasOwnProperty(styleName) &&
(!nextProp || !nextProp.hasOwnProperty(styleName))
) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = '';
}
}
// scr: the comment applies here -----------------------> b)
// Update styles that changed since `lastProp`.
for (styleName in nextProp) {
if (
nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]
) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = nextProp[styleName];
}
}
} else { // scr: -----------------------------------------> 4)
// Relies on `updateStylesByID` not mutating `styleUpdates`.
styleUpdates = nextProp;
}
} else if (...) {
// scr: DEV code
...
}
}
if (styleUpdates) { // scr: ----------------------------------> 5)
CSSPropertyOperations.setValueForStyles(
getNode(this),
styleUpdates,
this,
);
}
},
ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js

1) if the new props do not contain “style” at all,

...
if (nextProps.hasOwnProperty(propKey) ||...) {
continue;
} // scr: else, do something
...

mark all the existing style entries as ‘remove’, note that existing styles are stored in this._previousStyleCopy in step 2);

2) copy nextProp (current styles) to this._previousStyleCopy;

3) if there are existing styles,

var lastProp = propKey === STYLE
? this._previousStyleCopy
...
if (lastProp) {
...

update by a) marking existing style entries that are not in nextProp as ‘remove’ and b) marking style entries in nextProp as ‘add’ if it is different from the existing entry on the same key;

4) if not, simply mark all the styles in nextProp as ‘add’;

5) conduct the real DOM operations. Note that getNode() is an alias to ReactDOMComponentTree.getNodeFromInstance() that uses ReactDOMComponent._hostNode to get the associated DOM element {Figure-III} {post three}.

ctl-f “ReactDOMComponent[ins]._hostNode”

`CSSPropertyOperations.setValueForStyles()` — update props

setValueForStyles: function(node, styles, component) {
var style = node.style;
for (var styleName in styles) {
if (!styles.hasOwnProperty(styleName)) {
continue;
}
// scr: DEV code or code that is not applicable
...

if (isCustomProperty) {
...
} else if (styleValue) {
style[styleName] = styleValue;
} else {
code that is not applicable this time
...
}
}
},
CSSPropertyOperations@renderers/dom/shared/CSSPropertyOperations.js

Here the only line that is applicable here is style[styleName] = styleValue; that set the node.style with styles marked in the previous method.

As a result, Node.style[‘color’] = ‘red’.

`_updateDOMChildren` —check if a DOM’s content changed (and recurse its children)

We omit the dangerouslySetInnerHTML related code and focus only on hot paths

_updateDOMChildren: function(
lastProps,
nextProps,
transaction,
context
) {
var lastContent = CONTENT_TYPES[typeof lastProps.children]
? lastProps.children
: null;
var nextContent = CONTENT_TYPES[typeof nextProps.children]
? nextProps.children
: null;
// scr: code that is not applicable
...
// Note the use of `!=` which checks for null or undefined.
// scr: used by recursing children, to be continued...
var lastChildren = lastContent != null ? null : lastProps.children;
var nextChildren = nextContent != null ? null : nextProps.children;
// scr: code that is not applicable
...
if (lastChildren != null && nextChildren == null) {
// scr: recursing children, to be continued...
this.updateChildren(null, transaction, context);
} else if (lastHasContentOrHtml && !nextHasContentOrHtml) {
// scr: DEV code and code that is not applicable
...
}
if (nextContent != null) {
if (lastContent !== nextContent) {
this.updateTextContent('' + nextContent);
// scr: DEV code
...
}
} else if (nextHtml != null) {
// scr: code that is not applicable
...
} else if (nextChildren != null) {
// scr: DEV code
...
// scr: recursing children, to be continued...
this.updateChildren(nextChildren, transaction, context);
}
},
ReactDOMComponent@renderers/dom/shared/ReactDOMComponent.js

The only line that is applicable here is

 this.updateTextContent(‘’ + nextContent);

`ReactDOMComponent.updateTextContent()` — update content

Presumably ReactDOMComponent.updateTextContent() is used to set the text from 'start' to 'end'. But the call stack of this process is a bit too deep for this simple operation,

updateTextContent: function(nextContent) {
var prevChildren = this._renderedChildren;
// Remove any rendered children. scr: -------> the comment applies
ReactChildReconciler.unmountChildren(prevChildren, false);
for (var name in prevChildren) {
// scr: sanity check
...
}

// Set new text content. scr: ---------------> the comment applies
var updates = [makeTextContent(nextContent)];
processQueue(this, updates);
},
function processQueue(inst, updateQueue) {
ReactComponentEnvironment.processChildrenUpdates(inst, updateQueue);
}
ReactMultiChild@renderers/shared/stack/reconciler/ReactMultiChild.js

Here ReactComponentBrowserEnvironment is injected as ReactComponentEnvironment.

...
processChildrenUpdates:
ReactDOMIDOperations.dangerouslyProcessChildrenUpdates,
...
ReactComponentBrowserEnvironment@renderers/dom/shared/ReactComponentBrowserEnvironment.js

and .processChildrenUpdates is an alias to ReactDOMIDOperations.dangerouslyProcessChildrenUpdates

dangerouslyProcessChildrenUpdates: function(parentInst, updates) {
var node = ReactDOMComponentTree.getNodeFromInstance(parentInst);
DOMChildrenOperations.processUpdates(node, updates);
},
ReactDOMIDOperations@renderers/dom/client/ReactDOMIDOperations.js

The ReactDOMComponentTree.getNodeFromInstance() method is discussed in the previous section.

processUpdates: function(parentNode, updates) {
// scr: DEV code
...
for (var k = 0; k < updates.length; k++) {
var update = updates[k];
switch (update.type) {
// scr: code that is not applicable
...
case 'TEXT_CONTENT':
setTextContent(parentNode, update.content);
// scr: DEV code
...
break;
...
DOMChildrenOperations@renderers/dom/client/utils/DOMChildrenOperations.js

As expected, the last card in this stack is setTextContent() which sets Node.textContent directly. This method is covered in {post five} so I will not repeat its implementation.

The sub call stack of ReactDOMComponent.updateTextContent() and the ‘end’ result of it:

|-ReactDOMComponent.updateTextContent()
|-processQueue()
|-ReactComponentEnvironment.processChildrenUpdates()
|=ReactDOMIDOperations.dangerouslyProcessChildrenUpdates()
|-ReactDOMComponentTree.getNodeFromInstance()
|-DOMChildrenOperations.processUpdates()
|-setTextContent()
|-Node.textContent = 'end'

In the next post we are going to further investigate the diffing algorithm by observing the mutation of DOM trees, which also concludes this series (for a period of time). I hope you will feel more

next time when using setState(). Thanks, and I hope to see you the next time.

Originally published at holmeshe.me.

--

--