Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
Understanding The React Source Code — UI Updating (Individual DOM) VIII
Photo by Cristina Gottardi on Unsplash
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 updating can be conducted 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 solid ground to establish an initial understanding.
Let’s get started with an example extended 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.
Before transactions
Firstly, 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.
Next we look at 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) Starts the Transaction(s) {post six, seven} with one line method calling:
...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.
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()
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:
The ReactElements are the virtual DOMs we are going to agree on. {post five} also discussed how the virtual DOMs of this kind are initially established.
In MVC terms ReactElements are modals, and ReactDOMComponents are controllers.
Diffing
The figure above gives the old virtual DOM tree. A new one (‘s modal) will be generated with ReactCompositeComponent[ins]._renderValidatedComponent(). The process is the same as in {post four}.
ctl-f “in _renderValidatedComponent()”
_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 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;
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};
5) if the a the same so, start the DOM updating process.
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 relationship of these functions and the funtionality recursion in general.
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 changedThis 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 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'. However, the call stack of this method is a bit 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 the stack is setTextContent() which sets Node.textContent directly. This method is covered in {post V} 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.
Understanding The React Source Code VIII was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.