React treats components like state machines (finite state machines), using state to control local state and props to pass state. React maps state to the UI (initial render). React synchronizes state to the UI.

How does React update components?

How does React compare the least changed parts of the page?

This article will answer those questions for you.

Before that,

You’ve learned some basic concepts inside React (15-Stable version), including different types of component instances, mount procedures, transactions, and the general process for batch updates (not yet? Don’t worry, it’s ready for you to see component initial render from source, and then see component initial render from source);

Prepare a demo, debug source code, in order to better understand;

Keep calm and make a big deal !

How does React update components?

TL; DR
  • Rely on transactions for batch updates;
  • The lifecycle of a batch is fromReactDefaultBatchingStrategyTransaction perform before (call ReactUpdates. BatchUpdates) to the end of the this transaction after a close method call;
  • When the transaction starts, it encounters setState and stores the partial state on the component instance’s _pendingStateQueue, then stores the component in the dirtyComponents array, and so onReactDefaultBatchingStrategyCalled when the transaction endsrunBatchedUpdatesBatch update all components;
  • Component updates are recursive, with each of the three different types of components having its ownupdateComponentMethod to determine how to update its own component, where the ReactDOMComponent uses the diff algorithm to compare the smallest changes in child elements and batch them.

The update process works like a process, whether you update a component with setState(or replaceState) or new props.

So what is it?

Let’s start with the beginning of this update process…

Before I call setState

First of all, starting a batch entry is in ReactDefaultBatchingStrategy, calls the batchedUpdates inside can open a batch:

// Batch processing policy
var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false.batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true; // Start batch once

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      // Start the transaction and execute the callback in the transaction
      return transaction.perform(callback, null, a, b, c, d, e); }}};Copy the code

In React, there are a number of places to call batchedUpdates, which are relevant to the update process as follows

// ReactMount.js
ReactUpdates.batchedUpdates(
      batchedMountComponentIntoNode,  // Take charge of initial rendering
      componentInstance,
      container,
      shouldReuseMarkup,
      context,
);

// ReactEventListener.js
dispatchEvent: function(topLevelType, nativeEvent) {... try { ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);// Handle events
    } finally{ TopLevelCallbackBookKeeping.release(bookKeeping); }},Copy the code

In the first case, React calls batchedUpdates when rendering the component for the first time and then starts rendering the component. So why start Batch at this point? Not because the insertion process is recursive, but because components call various lifecycle functions sequentially during rendering, and developers are likely to call setState in a lifecycle function (such as componentWillMount or componentDidMount). Therefore, to start Batch once is to store updates (put in dirtyComponents) and then batch update at the end of the transaction. This way, during the initial rendering process, any setState will take effect and the user will always see the latest state.

In the second case, if you bind events to HTML elements or components, you might call setState in the listener function of the event, so you also need to enable a batch update policy to store updates (putting dirtyComponents). Before the callback function is called, the dispatchEvent function in the React event system is responsible for event distribution. In dispatchEvent, the transaction is started, batch is started once, and the callback function is called. This way, the setState called in the listener function of the event takes effect.

That is, anywhere setState may be called, React will initiate a batch update policy to anticipate possible setState before it is called

So what happens after batchedUpdates are called?

The React batchedUpdates will pass in a function called, will start batchedUpdates ReactDefaultBatchingStrategyTransaction transaction, this function will be implemented in a transaction:

// ReactDefaultBatchingStrategy.js
var transaction = new ReactDefaultBatchingStrategyTransaction(); // instantiate the transaction
var ReactDefaultBatchingStrategy = {
  ...
  batchedUpdates: function(callback, a, b, c, d, e) {... return transaction.perform(callback,null, a, b, c, d, e);  // Execute callback in transaction. };Copy the code

ReactDefaultBatchingStrategyTransaction this transaction control the life cycle of the batch strategy:

// ReactDefaultBatchingStrategy.js
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),  // Batch update
};
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;  // End this batch}};var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
Copy the code

No matter what function you pass in, it will always call the close method of the above transaction after execution. Call flushBatchedUpdates first, and then end batch.

What happens after setState is called

// ReactBaseClasses.js :
ReactComponent.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState'); }};// => ReactUpdateQueue.js:
enqueueSetState: function(publicInstance, partialState) {
    // Use this in this.setState to get the internal instance, that is, the component instance
	var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
    // Get the component instance's _pendingStateQueue
    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    // Save partial state to _pendingStateQueue
    queue.push(partialState);
	/ / call enqueueUpdate
    enqueueUpdate(internalInstance);
 }

// => ReactUpdate.js:
function enqueueUpdate(component) {
  ensureInjected(); // Inject the default policy
    
    // Start Batch once if batch is not started (or the current batch has ended), which usually occurs when setState // is called in an asynchronous callback
  if(! batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component);return;
  }
    // If batch is enabled, updates are stored
  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1; }}Copy the code

That is, calling setState takes the internal component instance, stores the updated partial state in its _pendingStateQueue, and marks the current component as dirtyComponent. Save it in the dirtyComponents array. Then we continue to do the following, not immediately updated, because the next code will probably call setState, so we just do the storage.

When will batch update?

First, when a transaction is executed (including the Initialize, Perform, and close phases), any phase may call a series of functions and open other transactions. The previously started transaction will continue to be executed only after the subsequent started transaction completes. Here is the sequence of transactions that React starts and executes after setState is set during the initial rendering of the component:

Can see from the table, the batch update is the close of the phase in ReactDefaultBatchingStrategyTransaction affairs, Launched in flushBatchedUpdates functions ReactUpdatesFlushTransaction affairs is responsible for batch update.

How to batch update?

Enable batch update transactions and batch callback processing

Let’s look at the flushBatchedUpdates function, in reactupdates.js

var flushBatchedUpdates = function () {
  // Start the batch update transaction
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }
// Batch callback
    if (asapEnqueued) {
      asapEnqueued = false;
      varqueue = asapCallbackQueue; asapCallbackQueue = CallbackQueue.getPooled(); queue.notifyAll(); CallbackQueue.release(queue); }}};Copy the code
Traverse dirtyComponents

FlushBatchedUpdates flushBatchedUpdates starts an update transaction that executes runBatchedUpdates for batch updates:

// ReactUpdates.js
function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;
  // Sort to ensure that the parent component is updated before the child component
  dirtyComponents.sort(mountOrderComparator);

  // Indicates the number of batch updates. Ensure that each component is updated only once
  updateBatchNumber++;
  / / traverse dirtyComponents
  for (var i = 0; i < len; i++) {
    var component = dirtyComponents[i];
      
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null; .// Perform the updateReactReconciler.performUpdateIfNecessary( component, transaction.reconcileTransaction, updateBatchNumber, ); .// Store callback for subsequent calls in sequence
    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) { transaction.callbackQueue.enqueue( callbacks[j], component.getPublicInstance(), ); }}}}Copy the code

After pushing the component into the dirtyComponents array by setState, we now iterate through the dirtyComponents array to update it.

Perform updates as required

PerformUpdateIfNecessary ReactReconciler invokes the component instance. If the props is received, the receiveComponent of the component is called, from which updateComponent is called to update the component. If no props were accepted, but there is a new state to update (_pendingStateQueue is not empty), updateComponent is called directly to update:

// ReactCompositeComponent.js
performUpdateIfNecessary: function (transaction) {
    if (this._pendingElement ! =null) {
        ReactReconciler.receiveComponent(this.this._pendingElement, transaction, 				this._context);
    } else if (this._pendingStateQueue ! = =null || this._pendingForceUpdate) {
        this.updateComponent(transaction, this._currentElement, this._currentElement, 			this._context, this._context);
    } else {
        this._updateBatchNumber = null; }}Copy the code
Call updateComponent for the component instance

Next comes the updateComponent, which determines how the component updates itself and its descendants. It’s important to note that there are three different component types inside React, each of which has its own updateComponent with different behaviors.

For ReactCompositeComponent:

UpdateComponent does:

  • Call a series of lifecycle functions for this hierarchy of components and update props, state, and context when appropriate;
  • Re-render, compared to the element of render, if the key && element.type is the same, go to the next layer to update; If not, remove and mount it again

For ReactDOMComponent:

UpdateComponent does:

  • Update this level of DOM element attributes;
  • Update the child element, calling ReactMultiChildupdateChildren, compare before and after changes, mark change types, and save them in updates (the main part of the DIff algorithm);
  • Batch processing updates

For ReactDOMTextComponent:

How does React update all components at once? The answer is recursion.

Recursively invokes the component’s updateComponent

Looking at the update flow of the ReactCompositeComponent and ReactDOMComponent, React makes a decision every time it reaches the end of a component update process: ReceiveComponent is called if nextELement and prevElement key and type are equal. ReceiveComponent is the same as updateComponent. Each component has one. This is equivalent to updateComponent accepting the version of the new props. ReceiveComponent is called and the child element is updated. So the whole process looks like this (vector diagram) :

This process ensures that React only iterates through the tree once, but at the cost of deleting and re-creating elements as soon as there is a difference between the type and key of the elements rendered before and after React. Therefore, React recommends keeping the structure as stable as possible.

How does React compare the least changed parts of the page?

You might say React uses the virtual DOM to represent the page structure. Every time React updates, it re-renders the new Virtual DOM, uses the diff algorithm to compare the changes before and after, and then updates the page in batches. Yes, good, that’s the general process, but there are some hidden, deep questions worth discussing:

  • How does React represent the page structure with the virtual DOM so that any page changes can be diff out?
  • How does React diff out the part of the page that changes the least?

How does React represent the page structure

class C extends React.Component {
    render () {
        return (
            <div className='container'>
                  "dscsdcsd"
                  <i onClick={(e)= > console.log(e)}>{this.state.val}</i>
                  <Children val={this.state.val}/>
            </div>) } } // virtual DOM(React element) { ? Typeof: Symbol(react. Element) key: null props: {// props represents all properties on the element, and the children attribute is used to describe children: ["" DSCSDCSD "", {? Typeof: Symbol(react. Element), type: "I ", key: null, ref: null, props: {...},...}, {? Typeof: Symbol(react. Element), type: class Children, props: {...},...}] className: 'container'} ref: null type: "div" _owner: ReactCompositeComponentWrapper {... } _store: {validated: false} _self: null _source: null}Copy the code

Every tag, whether it’s a DOM element or a custom component, has attributes like key, Type, props, ref, and so on.

  • Key represents the unique ID value of the element, meaning that if the ID changes, the element must be different, even if the type of element is the same before and after.
  • There are function(empty wrapper), class(custom class), string(concrete DOM element name) types, just like key.
  • Props is an attribute of the element. Any properties written on the tag (such as className=’container’) are stored here. If the element has children (including text), props has the children attribute, which stores the children. The children attribute is the basis of recursive insertion and recursive update.

If an element’s unique identifier or category or attribute changes, then the attributes in the key, type, and props corresponding to the re-render will also change. In summary, React means that the structure of the page does reflect all changes.

So how does React diff?

React Diff only compares nodes of the same tier at a time:

The figures above indicate the order of traversal updates.

Starting with the parent node, each layer of diff consists of two places

  • Element diff — A comparison of elements rendered before and after to find out if they are the same node. Element diff compares the key and type of elements rendered before and after. Component top-level DOM element comparison and child element comparison:

    Component top-level DOM element comparison:

    // ReactCompositeComponent.js/updateComponent => _updateRenderedComponent
    _updateRenderedComponent: function(transaction, context) {
        / / re - render out of the element
    	var nextRenderedElement = this._renderValidatedComponent();
    	// Change before and after comparison
        if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
          // If the key && type does not change, the next level is updatedReactReconciler.receiveComponent(...) ; }else {
          // If it changes, remove it and rebuild it
          ReactReconciler.unmountComponent(prevComponentInstance, false); . var child =this._instantiateReactComponent(...) ;varnextMarkup = ReactReconciler.mountComponent(...) ;this._replaceNodeWithMarkup(...);
        }
    }
    Copy the code

    Comparison of child elements:

    // ReactChildReconciler.js
    updateChildren: function(.) {... for (namein nextChildren) {  // Iterate over elements generated by re-render. if ( prevChild ! =null &&
            shouldUpdateReactComponent(prevElement, nextElement)
          ) {
            // If the key && type does not change, the next level is updatedReactReconciler.receiveComponent(...) ; nextChildren[name] = prevChild;// After updating nextChildren, note that the component instance is added
          } else {
            // If it changes, remove and rebuild
            if (prevChild) {
              removedNodes[name] = ReactReconciler.getHostNode(prevChild);
              ReactReconciler.unmountComponent(prevChild, false);
            }
            var nextChildInstance = instantiateReactComponent(nextElement, true);
            nextChildren[name] = nextChildInstance;
              
            var nextChildMountImage = ReactReconciler.mountComponent(...);
            mountImages.push(nextChildMountImage);
          }
        }
        // Remove the prevChildren component, but not nextChildren component
        for (name in prevChildren) {
          if( prevChildren.hasOwnProperty(name) && ! (nextChildren && nextChildren.hasOwnProperty(name)) ) { prevChild = prevChildren[name]; removedNodes[name] = ReactReconciler.getHostNode(prevChild); ReactReconciler.unmountComponent(prevChild,false); }}},Copy the code

    ShouldComponentUpdate function:

    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 both changes are strings or numbers, updates are allowed
      if (prevType === 'string' || prevType === 'number') {
        return nextType === 'string' || nextType === 'number';
      } else {
        // Otherwise check type && key
        return (
          nextType === 'object'&& prevElement.type === nextElement.type && prevElement.key === nextElement.key ); }}Copy the code

    Element Diff checks that the type && key has not changed and moves to the next level. If it does, it simply removes the new element and reconstructs it, then iterates through the next level.

  • Subtree diff – a comparison of all of the children (elements in props. Children) wrapped by the top-level DOM element of the component to the previous version. This comparison is used to find changes to all of the children of the component, including removal, creation, and scale-wide movement.

    // ReactMultiChild.js
    _updateChildren: function(.) {
          var prevChildren = this._renderedChildren;
          var removedNodes = {};
          var mountImages = [];
          // Get the updated subcomponent instance
          var nextChildren = this._reconcilerUpdateChildren(); .// Iterate over child component instances
          for (name in nextChildren) {
       		...
            var prevChild = prevChildren && prevChildren[name];
            var nextChild = nextChildren[name];
            // Because updates to subcomponents are changed on the original component instance, they can be determined by reference comparison with the previous component
            if (prevChild === nextChild) {
                // A movement occurred
              updates = enqueue(
                updates,
                this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex),
              );
              lastIndex = Math.max(prevChild._mountIndex, lastIndex);
              prevChild._mountIndex = nextIndex;
            } else{...// There are new components
              updates = enqueue(
                updates,
                this._mountChildAtIndex(
                  nextChild,
                  mountImages[nextMountIndex],
                  lastPlacedNode,
                  nextIndex,
                  transaction,
                  context,
                ),
              );
              nextMountIndex++;
            }
            nextIndex++;
            lastPlacedNode = ReactReconciler.getHostNode(nextChild);
          }
          // Remove children that are no longer present.
          for (name in removedNodes) {
              // removedNodes records all removedNodes
            if (removedNodes.hasOwnProperty(name)) {
              updates = enqueue(
                updates,
                this._unmountChild(prevChildren[name], removedNodes[name]), ); }}if (updates) {
            processQueue(this, updates); // Batch processing
          }
          this._renderedChildren = nextChildren;
        },
    
    Copy the code

    React puts the same level of change tags, such as MOVE_EXISTING, REMOVE_NODE, TEXT_CONTENT, INSERT_MARKUP, etc., into the Updates array and processes them in batches.

And that’s it!

React is an exciting library that offers unprecedented development experiences, but as we bask in the joy of using React to quickly implement requirements, it’s worth asking two questions: Why and How?

Why is React so popular and why? Componentized, fast, simple enough, All in JS, easy to expand, ecologically rich, community strong…

What ideas does React reflect? State machine, webComponents, Virtual DOM, Virtual Stack, asynchronous rendering, multi-terminal rendering, one-way data flow, reactive update, functional programming…

What inspired the React ideas? How did you figure that out? How did you do that? .

Looking beyond the phenomenon to the essence, we can gain more meaningful knowledge than implementing requirements with React.

To be continued….