React Source Analysis 4-React Life Cycle Details

Keywords: React Java REST

1 React Life Cycle Process

The invocation process can be seen in the figure above. It can be divided into three different stages: instantiation, existence and destruction. There are many articles about the life cycle process. I believe most of the students also know something about it. We will not analyze it in detail. Many students have doubts about which methods are called inside React and when they trigger. Now let's make a detailed analysis.

2 Instance Life Cycle

getDefaultProps

When React.creatClass() initializes the component class, getDefaultProps() is called to mount the default properties returned to the defaultProps variable. This code has been analyzed before, refer to React Source Analysis 2 - Component and Object Creation (createClass, createElement).

The point here is that the initialization component class runs only once. It can be simply analogized to a Class object in Java. Initialization of component classes is the process by which ClassLoader loads Class objects. Class object initialization should not be misinterpreted as instance object initialization. A React component class may be called multiple times in JSX to generate multiple component objects, but it has only one class object, that is, getDefaultProps will not be called after class loading.

getInitialState

This method is called when the component instance object is created, and the specific code is in the Constructor function of React.creatClass(). Previous articles have been analyzed for reference. React Source Analysis 2 - Component and Object Creation (createClass, createElement).

Every time a React instance object is created, it is called.

mountComponent

CompoonentWillMount, render, and componentDidMount are all called in mountComponent. stay React Source Analysis 3-React Component Insertion DOM Process In this article, we talked about the time when mountComponent was called. It is called in rendering the new React Component. Enter React Component and return the HTML corresponding to the component. By inserting this HTML into DOM, DOM objects corresponding to components can be generated. So mountComponent is especially critical.

Like polymorphism in Java, mountComponent implementations of different React components differ. Let's focus on the mountComponent of React custom component class, which is the mountComponent of React Composite Component.

  mountComponent: function (transaction, nativeParent, nativeContainerInfo, context) {
    this._context = context;
    this._mountOrder = nextMountID++;
    this._nativeParent = nativeParent;
    this._nativeContainerInfo = nativeContainerInfo;

    // To make a judgment about whether propTypes are legal, this is useful only at the development stage.
    var publicProps = this._processProps(this._currentElement.props);
    var publicContext = this._processContext(context);

    var Component = this._currentElement.type;

    // Initialize common classes
    var inst = this._constructComponent(publicProps, publicContext);
    var renderedElement;

    // The empty Inst or inst.render corresponds to the stateless component, which is the stateless component.
    // A stateless component has no instance object, it is essentially just a function that returns JSX. Is a lightweight React component
    if (!shouldConstruct(Component) && (inst == null || inst.render == null)) {
      renderedElement = inst;
      warnIfInvalidElement(Component, renderedElement);
      inst = new StatelessComponent(Component);
    }

    // set variable
    inst.props = publicProps;
    inst.context = publicContext;
    inst.refs = emptyObject;
    inst.updater = ReactUpdateQueue;
    this._instance = inst;

    // Store the reference of instance object in map for later search
    ReactInstanceMap.set(inst, this);

    // Initialize state, queue, etc.
    var initialState = inst.state;
    if (initialState === undefined) {
      inst.state = initialState = null;
    }
    this._pendingStateQueue = null;
    this._pendingReplaceState = false;
    this._pendingForceUpdate = false;

    var markup;
    if (inst.unstable_handleError) {
      // Error while mounting, do some error handling, and then perform Initial Mount to initialize the mount
      markup = this.performInitialMountWithErrorHandling(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    } else {
      // Initial mount
      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    }

    if (inst.componentDidMount) {
      // Call componentDidMount as a transaction
      transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
    }

    return markup;
  },

mountComponent initializes the props and state of the instance object first, then calls PerfmInitialMount to initialize the mount, and then calls CompoonentDidMount after completion. The call chain is still clear. Let's focus on performance Initial Mount WithError Handling and performance Initial Mount

  performInitialMountWithErrorHandling: function (renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
    var markup;
    var checkpoint = transaction.checkpoint();
    try {
      // Put it in try-catch and call performInitialMount to initialize the mount if there is no error. So there's nothing special here, that's to say, to do some error handling.
      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    } catch (e) {
      // handleError, uninstall the component, and then re-perform Initial Mount to initialize the mount
      transaction.rollback(checkpoint);
      this._instance.unstable_handleError(e);
      if (this._pendingStateQueue) {
        this._instance.state = this._processPendingState(this._instance.props, this._instance.context);
      }
      checkpoint = transaction.checkpoint();

      this._renderedComponent.unmountComponent(true);
      transaction.rollback(checkpoint);

      markup = this.performInitialMount(renderedElement, nativeParent, nativeContainerInfo, transaction, context);
    }
    return markup;
  },

It can be seen that performance Initial Mount Within Error Handling is just one layer more error handling, the key is still in performance Initial Mount.

  performInitialMount: function (renderedElement, nativeParent, nativeContainerInfo, transaction, context) {
    var inst = this._instance;
    if (inst.componentWillMount) {
      // Call componentWillMount before render
      inst.componentWillMount();
      // Merge state ahead of time, so calling setState in component WillMount does not trigger a render, but does a state merge. The goal is to reduce unnecessary re-rendering.
      if (this._pendingStateQueue) {
        inst.state = this._processPendingState(inst.props, inst.context);
      }
    }

    // If it is not stateless, that is, stateless components, render is called and ReactElement is returned.
    if (renderedElement === undefined) {
      renderedElement = this._renderValidatedComponent();
    }

    // Get component types, such as empty component ReactNodeTypes.EMPTY, custom React component ReactNodeTypes.COMPOSITE, DOM native component ReactNodeTypes.NATIVE
    this._renderedNodeType = ReactNodeTypes.getType(renderedElement);
    // ReactComponent is generated from ReactElement, which was explained before. Create different Component objects based on different type s
    // Refer to http://blog.csdn.net/u013510838/article/details/55669769
    this._renderedComponent = this._instantiateReactComponent(renderedElement);

    // Recursive Rendering, Rendering Subcomponents
    var markup = ReactReconciler.mountComponent(this._renderedComponent, transaction, nativeParent, nativeContainerInfo, this._processChildContext(context));

    return markup;
  },

In performance Initial Mount, componentWillMount() is called first, then the state changes generated by setState() are merged into states, and then _renderValidatedComponent() is called back to ReactElement, which calls the render() method. React Component is then created by React Element. Finally, recursive rendering is performed. Let's look at renderValidatedComponent()

  _renderValidatedComponent: function () {
    var renderedComponent;
    ReactCurrentOwner.current = this;
    try {
      renderedComponent = this._renderValidatedComponentWithoutOwnerOrContext();
    } finally {
      ReactCurrentOwner.current = null;
    }
    !(
    return renderedComponent;
  },

  _renderValidatedComponentWithoutOwnerOrContext: function () {
    var inst = this._instance;
    // Call render method to get ReactElement. JSX is actually the createElement() method after babel translation. This point has also been explained before.
    var renderedComponent = inst.render();
    return renderedComponent;
  },

3 Life Cycle of Existence Period

When the component instance object has been generated, we can update the component through setState(). The setState mechanism will be analyzed later in a separate article, and now you just need to know that it will call updateComponent() to complete the update. Here's an analysis of updateComponent

updateComponent: function(transaction, prevParentElement, nextParentElement, prevUnmaskedContext, nextUnmaskedContext
  ) {
    var inst = this._instance;
    var willReceive = false;
    var nextContext;
    var nextProps;

    // If the context object changes, check propTypes, etc., which can alert you to errors during development
    if (this._context === nextUnmaskedContext) {
      nextContext = inst.context;
    } else {
      nextContext = this._processContext(nextUnmaskedContext);
      willReceive = true;
    }

    // If the parent element type is the same, skip propTypes type checking
    if (prevParentElement === nextParentElement) {
      nextProps = nextParentElement.props;
    } else {
      nextProps = this._processProps(nextParentElement.props);
      willReceive = true;
    }

    // Call componentWillReceiveProps, and if the updateComponent entered through setState, there is no such step
    if (willReceive && inst.componentWillReceiveProps) {
      inst.componentWillReceiveProps(nextProps, nextContext);
    }

    // Merge state ahead of time. Calling setState in component WillReceive Props will not re-render. Merge here, because render is also called later.
    // This avoids unnecessary rendering
    var nextState = this._processPendingState(nextProps, nextContext);

    // Call shouldComponentUpdate to assign shouldUpdate
    // If the updateComponent entered through forceUpdate, i.e. _pendingForceUpdate is not empty, it is not necessary to judge shouldComponentUpdate.
    var shouldUpdate = true;
    if (!this._pendingForceUpdate && inst.shouldComponentUpdate) {
      shouldUpdate = inst.shouldComponentUpdate(nextProps, nextState, nextContext);
    }

    // If shouldUpdate is true, the rendering will be performed, otherwise it will not.
    this._updateBatchNumber = null;
    if (shouldUpdate) {
      this._pendingForceUpdate = false;
      // Perform update rendering, detailed analysis later
      this._performComponentUpdate(
        nextParentElement,
        nextProps,
        nextState,
        nextContext,
        transaction,
        nextUnmaskedContext
      );
    } else {
      // Should Update is false, and rendering will not be updated
      this._currentElement = nextParentElement;
      this._context = nextUnmaskedContext;
      inst.props = nextProps;
      inst.state = nextState;
      inst.context = nextContext;
    }
},

In updateComponent, first call component WillReceiveProps, and then merge the state changes caused by setState. Then call shouldComponentUpdate to determine whether the rendering needs to be updated. If necessary, call _performComponentUpdate to perform rendering updates. Next, analyze performance ComponentUpdate.

_performComponentUpdate: function(nextElement,nextProps,nextState,nextContext,transaction,
                                     unmaskedContext
  ) {
    var inst = this._instance;

    // Judge if update s have been made
    var hasComponentDidUpdate = Boolean(inst.componentDidUpdate);
    var prevProps;
    var prevState;
    var prevContext;
    if (hasComponentDidUpdate) {
      prevProps = inst.props;
      prevState = inst.state;
      prevContext = inst.context;
    }

    // Call componentWillUpdate before render
    if (inst.componentWillUpdate) {
      inst.componentWillUpdate(nextProps, nextState, nextContext);
    }

    // Attributes such as state props are set to the internal variable inst
    this._currentElement = nextElement;
    this._context = unmaskedContext;
    inst.props = nextProps;
    inst.state = nextState;
    inst.context = nextContext;

    // The render method is called internally to resolve ReactElement and get HTML
    this._updateRenderedComponent(transaction, unmaskedContext);

    // Call componentDidUpdate after render
    if (hasComponentDidUpdate) {
      transaction.getReactMountReady().enqueue(
        inst.componentDidUpdate.bind(inst, prevProps, prevState, prevContext),
        inst
      );
    }
},

_ PerfmComponentUpdate calls componentWillUpdate, then updates the rendering by calling updateRendered Component, and finally calls componentDidUpdate. Let's see how to call the render method in updateRenderedComponent.

_updateRenderedComponent: function(transaction, context) {
    var prevComponentInstance = this._renderedComponent;
    var prevRenderedElement = prevComponentInstance._currentElement;

    // _ render Validated Component calls render internally to get ReactElement
    var nextRenderedElement = this._renderValidatedComponent();

    // Determine whether to do DOM diff. React, in order to simplify recursive diff, assumes that the component hierarchy remains unchanged, and that type and key remain unchanged (keys are used for listView and other components, many times we don't set type) before updating, otherwise un mount before reloading
    if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
      // Recursive updateComponent to update the Virtual DOM of subcomponents
      ReactReconciler.receiveComponent(prevComponentInstance, nextRenderedElement, transaction, this._processChildContext(context));
    } else {
      var oldNativeNode = ReactReconciler.getNativeNode(prevComponentInstance);

      // Do not do DOM diff, then uninstall first, and then load. That is to say, unMount Component, then mount Component
      ReactReconciler.unmountComponent(prevComponentInstance, false);

      this._renderedNodeType = ReactNodeTypes.getType(nextRenderedElement);

      // Creating ReactComponent from ReactElement
      this._renderedComponent = this._instantiateReactComponent(nextRenderedElement);

      // mountComponent mounts components to get the corresponding HTML
      var nextMarkup = ReactReconciler.mountComponent(this._renderedComponent, transaction, this._nativeParent, this._nativeContainerInfo, this._processChildContext(context));

      // Insert HTML into DOM
      this._replaceNodeWithMarkup(oldNativeNode, nextMarkup, prevComponentInstance);
    }
},

_renderValidatedComponent: function() {
    var renderedComponent;
    ReactCurrentOwner.current = this;
    try {
      renderedComponent =
        this._renderValidatedComponentWithoutOwnerOrContext();
    } finally {
      ReactCurrentOwner.current = null;
    }

    return renderedComponent;
},

_renderValidatedComponentWithoutOwnerOrContext: function() {
    var inst = this._instance;
    // When you see render method, you should rest assured.~
    var renderedComponent = inst.render();

    return renderedComponent;
},

Like mountComponent, updateComponent updates subcomponents recursively. Special attention should be paid here to DOM diff. DOM diff is the key to rendering acceleration in React. It will help us figure out the real changes in virtual DOM and perform native DOM operations on this part. In order to avoid the inefficiency of cyclic recursive comparison of nodes, the assumption in React is that only components with the same level, type and key are updated by Virtual DOM. The key to this is shouldUpdate React Component, which is analyzed below

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;
  // React DOM diff algorithm
  // If the first and last two times are numbers or characters, it is assumed that only update (processing text elements) is required.
  // If DOM or React elements are used twice before and after updating, they must be in the same level, and type and key remain unchanged (key is used for listView and other components, and in many cases we do not set type) before updating, otherwise unmount first and then re-mount.
  if (prevType === 'string' || prevType === 'number') {
    return (nextType === 'string' || nextType === 'number');
  } else {
    return (
      nextType === 'object' &&
      prevElement.type === nextElement.type &&
      prevElement.key === nextElement.key
    );
  }
}

4 destruction

As mentioned earlier, if the DOM diff condition is not met, unmountComponent will be upgraded first and then mountComponent. Now let's analyze what happens when unmountComponent is upgraded. Like mountComponent polymorphism, different type s of React Component have different unmountComponent behavior. Let's analyze the unmountComponent in React Composite Component.

  unmountComponent: function(safely) {
    if (!this._renderedComponent) {
      return;
    }
    var inst = this._instance;

    // Call component WillUnmount
    if (inst.componentWillUnmount && !inst._calledComponentWillUnmount) {
      inst._calledComponentWillUnmount = true;
      // In security mode, package component WillUnmount in try-catch. Otherwise, componentWillUnmount directly
      if (safely) {
        var name = this.getName() + '.componentWillUnmount()';
        ReactErrorUtils.invokeGuardedCallback(name, inst.componentWillUnmount.bind(inst));
      } else {
        inst.componentWillUnmount();
      }
    }

    // Recursively call unMountComponent to destroy subcomponents
    if (this._renderedComponent) {
      ReactReconciler.unmountComponent(this._renderedComponent, safely);
      this._renderedNodeType = null;
      this._renderedComponent = null;
      this._instance = null;
    }

    // reset wait queue and other wait states
    this._pendingStateQueue = null;
    this._pendingReplaceState = false;
    this._pendingForceUpdate = false;
    this._pendingCallbacks = null;
    this._pendingElement = null;

    // reset internal variables to prevent memory leaks
    this._context = null;
    this._rootNodeID = null;
    this._topLevelWrapper = null;

    // Remove the component from the map, remember that we added it to the map in mountComponent
    ReactInstanceMap.remove(inst);
  },

So unmountComponent is relatively simple. It does three things.

  1. Call componentWillUnmount()
  2. Call unmountComponent() recursively to destroy subcomponents
  3. Vacuum internal variables to prevent memory leaks

5 Summary

React Custom Component Creation Period, Existence Period and Destruction Period are all described above. Three entry functions mountComponent, updateComponent and unmountComponent are particularly critical. If you are interested, you can also analyze the three methods of ReactDOM Component, ReactDOM Component and ReactDOM Component.

Learning React life cycle source code in depth can help us clarify the invocation order of each method, understand when they are invoked, which conditions will be invoked, and so on. Reading the source code is a bit boring, but it can greatly deepen the understanding of the upper API interface, and experience the designer's good intentions in designing these APIs.

Posted by sharugan on Mon, 08 Apr 2019 02:54:30 -0700