➣ React Fiber works


The React architecture

  • 1) Virtual DOM layer, describe what the page looks like
  • 2) Reconciler’s layer, which calls component lifecycle methods, conducts Diff operations, and so on
  • 3) Renderer layer, render corresponding pages according to different platforms, such as ReactDOM and ReactNative

React15 legacy issues

  • 1) The overall rendering of the browser is multi-threaded, including GUI rendering threads, JS engine threads, event-triggering threads, timed trigger threads, and asynchronous HTTP request threads. Page drawing and JS operations are mutually exclusive threads and cannot be performed at the same time.
  • 2) React15 uses JS’s Stack of function calls to recursively render the interface, so a large number of synchronized tasks (tree diff and page render) cause interface update blocking, event response delays, animation stalling, and more when dealing with frequent updates to complex pages with too many DOM elements, So the React team rewrote the React Reconciler architecture in version 16.

React16 problem resolved

  • 1)Fiber ReconcilerArchitecture allows the task of a synchronized block split into multiple small tasks, each task takes a short period of time, after the completion of the task execution judgment have free time, have continued to perform the next task, otherwise will control by the browser to allow the browser to deal with a higher priority task, such as the time slice, next time other subtasks to continue. The whole process is similar to CPU scheduling logic, using browser apis at the bottomrequestIdleCallback.
  • 2) In order to make the whole Diff and Render process interruptible and recoverable, pure VirtualDom Tree is no longer sufficient, so React16 introduces Fiber Tree with single linked list structure, as shown in the figure below.
  • 3) The FiberReconciler framework divides the renewal process into two phases: 1. Diff (consisting of multiple DIFF tasks, which can be interrupted after the task time slice is consumed, and can be awakened again by requestIdleCallback) => 2.com MIT (DOM rendering can not be interrupted after the fiber Tree update results are obtained after diff) The gray tree on the left is a Fiber tree, and the workInProgress on the right is an intermediate state. It is a tree structure built from the top down during the diFF process, which can be used for breakpoint recovery. After all work units are updated, the generated workInProgress tree will become a new Fiber tree.
  • 4) each node in the fiber tree is a unit of work, similar to the previous VirtualDom tree, representing a VirtualDom node. Each Fiber node of the workInProgress tree stores the Effect List generated during the diFF process, which is used to store the DIFF results, and the bottom tree nodes in turn merge the effect List to the upper layer to collect all the DIFF results. Note that if some nodes are not updated, the workInProgress Tree will reuse the original Fiber Tree nodes (linked list operation), and the nodes with updated data will be tagged.
<FiberNode> : {
    stateNode,    // Node instance
    child,        / / child nodes
    sibling,      // Sibling nodes
    return./ / the parent node
}
Copy the code

➣ React Old and new life cycles


Life cycle prior to Act 16.3

  1. componentWillMount()

This lifecycle function is called before the component is mounted and only fired once during the entire lifecycle. It is commonly used by developers to pre-request data to reduce request initiation time. Suggested alternatives are to consider putting it in the constructor constructor, or after componentDidMount; Another situation is when an external state management library, such as Mobx, can be used to reset saved data in the Mobx Store. The alternative is to use life Cycle componentWilUnmount to automatically clean up data when a component is uninstalled.

  1. componentDidMount()

This lifecycle function is called after the component has been mounted and fires only once during the entire lifecycle. Developers can also use it for data requests; Can also be used to add event subscriptions (unsubscribe event subscriptions in componentWillUnmount); Because the dom element is already rendered by the time the function fires, the third use case deals with the side effects of interface updates, such as using default data to initialize an Echarts component and then updating the echarts component after componentDidUpdate.

  1. componentWillReceiveProps(nextProps, nexState)

This life cycle occurs during the component update phase after the component is mounted. This is most commonly seen in a non-fully controlled component that relies on a PROP property to update the internal state of the component. A non-fully controlled component maintains state updates internally, but also uses external props to update the internal state under special conditions. Do not copy props to state directly. Otherwise, you should use a fully controlled Function Component, as shown in the following example:

class EmailInput extends Component {
  state = { email: this.props.email };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }

  handleChange = e= > his.setState({ email: e.target.value });

  componentWillReceiveProps(nextProps) {
    if(nextProps.userID ! = =this.props.userID) {
      this.setState({ email: nextProps.email }); }}}Copy the code
  1. shouldComponentUpdate(nextProps)

This life cycle occurs during the component update phase after the component is mounted. It is important to note that child updates are not necessarily due to props or state changes. It could also be that some other part of the parent component changes causing the parent to re-render so that the current child component is re-rendered without changing props/ State. When the function is called, it is passed the nextProps and nextState objects that are to be updated. Developers can decide whether to allow the update by comparing the properties of the two props that are related to interface rendering. Default is true. The object depth comparison function is often used to reduce the number of useless interface rendering times and optimize performance. PureComponnet is recommended for scenarios where only simple and shallow comparisons are required for props changes, and the same state and props render the same content. React will automatically perform a shallow comparison for you when the props are updated to reduce unnecessary rendering.

class EmailInput extends Component {
  state = { email: this.props.email };

  render() {
    return <input onChange={this.handleChange} value={this.state.email} />;
  }

  handleChange = e= > his.setState({ email: e.target.value });

  shouldComponentUpdate(nextProps, nextState) {
    if (
      nextProps.userID === this.props.userID &&
      nextState.email == this.state.email
    ) return false; }}Copy the code
  1. componenetWillUpdate(newProps, newState)

This life cycle occurs during the update phase after the component is mounted. When a component receives a new props or state and shouldComponentUpdate returns to allow the update, this method is called before rendering and setState cannot be executed during this lifecycle. In this lifecycle, developers can get the latest nextProps and nextState before the actual rendering of the interface is updated to perform some side effects, such as triggering an event, caching some calculation data into the component based on the latest props, and smoothing the animation of interface elements:

 // Use it with the CSS property Transition
 componentWillUpdate : function(newProps,newState){
    if(! newState.show) $(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'1'});
    else
      $(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'0'});;
  },
  componentDidUpdate : function(oldProps,oldState){
    if(this.state.show)
      $(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'1'});
    else
      $(ReactDOM.findDOMNode(this.refs.elem)).css({'opacity':'0'});;
  }
Copy the code
  1. componenetDidUpdate(prevProps, prevState)

This life cycle occurs during the update phase after the component is mounted, and the initial mount of the component is not triggered. This function is called when a component’s props and state change causes an interface rendering update. SetState cannot be executed during this lifecycle. We use it to perform some side effects, such as conditionally triggering the necessary network request to update local data, and using the latest data after render to invoke some external library execution (example: timer request interface data to dynamically draw echarts line graph) :

.componentDidMount() {
    this.echartsElement = echarts.init(this.refs.echart);
    this.echartsElement.setOption(this.props.defaultData); . }componentDidUpdate() {
    const { treeData } = this.props;
    const optionData = this.echartsElement.getOption();
    optionData.series[0].data = [treeData];
    this.echartsElement.setOption(optionData, true);
  }
Copy the code
  1. componentWillUnmount()

This life cycle occurs before the component is unloaded and is triggered only once in the component life cycle. Developers can perform data cleansing resets, unsubscribe page components from events, and more in this function.

Life cycle after Act 16.3

After Act 16.3, the React Reconciler architecture was rewritten (for dealing with life-cycle hook functions and DOM DIFF), with previous versions using a function call Stack recursive synchronous rendering mechanism known as Stack Reconciler, where the DOM DIFF phase cannot be interrupted. This is bad for animation execution and event response. After the React team used the Fiber Reconciler framework, the DIff phase disintegrated virtual DOM nodes into Fiber trees containing Fibernodes (implemented in a linked list), enabling arbitrary switching between Fiber task units, interruption and recovery between tasks. Fiber under the architecture of asynchronous rendering caused componentWillMount, componentWillReceiveProps, componentWillUpdate three life cycle may be invoked many times before the actual rendering, produce unexpected call results, Therefore, these three unsafe lifecycle functions are not recommended. Instead, use two entirely new lifecycle functions: getDerivedStateFromProps and getSnapshotBeforeUpdate.

  1. getDerivedStateFromProps(nextProps, currentState)
  • 1) define

The life cycle occur in the component initialization mount and component update phase, developers can use it to replace the previous componentWillReceiveProps life cycle, can be used to set dynamically according to the props change component internal state. The React function is static, so we can’t use this to access component instances directly, nor can we use this.setState to change state directly. This shows that the React team tries to minimize API abuse by developers through the React framework’s API-like constraints. The function call is passed as an argument the props to be updated and the state of the current component. We can trigger a component state update by comparing the props and returning an object. If null is returned, nothing is updated.

  • Abuse scenario 1: Copy props to state directly

This causes the state of the SimpleInput component to be reset to the props re-passed by the parent component when the parent rerenders, regardless of whether the props has changed. Is it ok to use shouldComponentUpdate with this to avoid this situation? Code level, but may cause the late shouldComponentUpdate function data source of confusion, any change will result in a prop to render and incorrect reset, maintain a reliable shouldComponentUpdate will be more difficult.

class SimpleInput extends Component {
  state = { attr: ' '  };

  render() {
    return <input onChange={(e)= > this.setState({ attr: e.target.value })} value={this.state.attr} />;
  }

  static getDerivedStateFromProps(nextProps, currentState) {
    // This overrides state updates in all components!
    return { attr: nextProps.attr }; }}Copy the code
  • 3) Usage scenario: Modify the state selectively after the props changes
class SimpleInput extends Component {
  state = { attr: ' '  };

  render() {
    return <input onChange={(e)= > this.setState({ attr: e.target.value })} value={this.state.attr} />;
  }

  static getDerivedStateFromProps(nextProps, currentState) {
    if(nextProps.attr ! == currentState.attr)return { attr: nextProps.attr };
    return null; }}Copy the code

Possible bug: In the case of a SimpleInput component that needs to be reset, the component does not reset properly because props. Attr has not changed, indicating that the value of the input box component is the same as the previous one.

  • 4) Optimized use scenario 1: Use fully controllable components

A fully controllable component is a functional component that has no internal state, and its state change is completely controlled by the parent props. In this way, the state originally located in the component and the logical method to change the state are extracted from the parent. It is suitable for simple scenarios, but logical redundancy can also be complicated if the parent has too much child state management logic.

function SimpleInput(props) {
  return <input onChange={props.onChange} value={props.attr} />;
}
Copy the code
  • 5) Optimized use scenario 2: Use non-controllable components with key values

If we want the component to have its own state management logic, but we can control the component to reinitialize with the new default value under appropriate conditions, there are several ways to do this:

/* 1. Set a unique value to be passed in as a sign that the component is reinitialized manually */
class SimpleInput extends Component {
  state = { attr: this.props.attr, id=""  }; // Initialize the default value

  render() {
    return <input onChange={(e)= > this.setState({ attr: e.target.value })} value={this.state.attr} />;
  }

  static getDerivedStateFromProps(nextProps, currentState) {
    if(nextProps.id ! == currentState.id)return { attr: nextProps.attr, id: nextProps.id };
    return null; }}/* 2. Set a unique value as the key value of the component. If the key value changes, the component will be reinitialized with the default value */
class SimpleInput extends Component {
  state = { attr: this.props.attr  }; // Initialize the default value

  render() {
    return <input onChange={(e)= > this.setState({ attr: e.target.value })} value={this.state.attr} />;
  }
}

<SimpleInput
  attr={this.props.attr}
  key={this.props.id}
/>

/* 3. Provide an external calling function for the parent to call directly to reset the component state. The parent accesses the component instance through refs and calls the internal method of the component
class SimpleInput extends Component {
  state = { attr: this.props.attr  }; // Initialize the default value

  resetState = (value) = > {
    this.setState({ attr: value });
  }

  render() {
    return <input onChange={(e)= > this.setState({ attr: e.target.value })} value={this.state.attr} />;
  }
}

<SimpleInput
  attr={this.props.attr}
  ref={this.simpleInput}
/>
Copy the code
  1. componentDidMount()

.

  1. shouldComponentUpdate(nextProps, nexState)

.

  1. getSnapshotBeforeUpdate(prevProps, prevState)

This life cycle occurs during the component initial mount and component update phases before the interface is actually rendered. Developers can get the prevProps and prevState of the component before the update, as well as the state of the DOM before rendering (element width, scrollbar length, position, etc.). The return value of this function will be passed in as the third parameter to the componentWillUpdate periodic function. By pairing with componentDidUpdate, you can completely replace the previous componentWillUpdate part of the logic, as shown in the following example.

class ScrollingList extends Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Determine whether to add new items to the list
    // Capture the scroll position so we can adjust the scroll position later.
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // Adjust the scroll position so that these new items do not push the old items out of the view
    // snapshot is the return value of getSnapshotBeforeUpdate)
    if(snapshot ! = =null) {
      const list = this.listRef.current; list.scrollTop = list.scrollHeight - snapshot; }}render() {
    return (
      <div ref={this.listRef}>{/ *... list items... * /}</div>); }}Copy the code
  1. componenetDidUpdate(prevProps, prevState, shot)

New to this life cycle: the return value of getSnapshotBeforeUpdate is passed in as the third argument when this function is executed.

  1. componenetWillUnmount

.