简体   繁体   中英

How does React update a component and its children after a state change?

I am watching Paul O Shannessy - Building React From Scratch

And I understand the mounting process very well but I have hard day trying to understand how React update a component and its children

The reconciler controls the update process by this method:

function receiveComponent(component, element) {
  let prevElement = component._currentElement;
  if (prevElement === element) {
    return;
  }

  component.receiveComponent(element);
}

Component.receiveComponent

 receiveComponent(nextElement) {
    this.updateComponent(this._currentElement, nextElement);
  }

and this is the Component.updateComponent method:

  updateComponent(prevElement, nextElement) {
    if (prevElement !== nextElement) {
      // React would call componentWillReceiveProps here
    }

    // React would call componentWillUpdate here

    // Update instance data
    this._currentElement = nextElement;
    this.props = nextElement.props;
    this.state = this._pendingState;
    this._pendingState = null;

    let prevRenderedElement = this._renderedComponent._currentElement;
    let nextRenderedElement = this.render();
 
    if (shouldUpdateComponent(prevRenderedElement, nextRenderedElement)) {
      Reconciler.receiveComponent(this._renderedComponent, nextRenderedElement);
    } 
  }

This is the part of the code that updates the component after state change, and i assume that it should update the children too, but i can't understand how this code achieves that, in the mounting process React instantiate components to dive deeper in the tree but this doesn't happen here, we need to find the first HTML element then we can change our strategy and update that HTML element in another place in the code, and I can't find any way to find any HTML elements this way.

Finding the first HTML is the way to stop this endless recursion and logically this is what I expect from the code, to stop recursion the same way in the mounting process, but in mounting, this demanded component instantiation so we can delegate to the reconciler that will discover that we are dealing with a wrapper instance of an HTML element not a wrapper instance of a custom component then React can place that HTML element in the DOM.

I can't understand how the code works in the update process. this code as I see won't dive deeper in the tree and I think won't update the children and can't let React find the first HTML element so React can update the DOM element, isn't it?

This is the code repo on Github

I think React not re-render parent component first instead of that, React re-render child component first.

Example: A (parent) -> B (child) -> C (child of B) When A update state C (re-render) -> B -> A

React completely copy the actual DOM and create the virtual DOM in javascript. In our application whenever we update any of the data that ends up being rendered in our components, React does not rerender the entire DOM. It only affects the thing that matters. So react actually copies the virtual DOM again. This time it applies the changes to the data that got updated.

在此处输入图像描述

It will make the change in the red component and then it will compare this virtual DOM to the old DOM. It will see the different part. Then it will apply the DOM changes only to that different component.

The updating phase starts if props or the state changes. If the data at the top level changes:

在此处输入图像描述

If it is passing that data down to its children, all the children are going to be rerendered. If the state of the component at the mid-level gets changed:

在此处输入图像描述

This time only its children will get rerendered. React will rerender any part of the tree below that node. Because the data that generates the children components' view actually sits at the parent component(mid-level one). But anything above it, the parent or the siblings will not rerender. because data does not affect them. this concept is called Unidirectional Data Flow .

You can see in action in chrome browser. chose the rendering and then enable the painting flushing option

在此处输入图像描述

If you make any change on the page, you will see that updated components will be flashed.

Hey Consider using a Tree data structure for your need, ReactJs follows a unidirectional manner of Updating the state ie As soon as the there is a Change in the parent state then all the children which are passed on the props that are residing in the Parent Component are updated once and for all! Consider using something known as Depth First Search as an algo option which will find you the Node that connects to the parent and once you reach that node, you check for the state and if there is a deviation from the state variables that are shared by the parent you can update them!

Note: This may all seem a bit theoretical but if you could do something remotely close to this thing you will have created a way to update components just how react does!

I found out experimentally that React will only re-render elements if it have to, which is always, except for {children} and React.memo() .

Using children correctly, together with batched dom updates makes a very efficient and smooth user experience.

consider this case:

function App() {
  return <div>
    <Parent>
      <Child01/>
      <Child01/>
    </Parent>
    <Child03/>
  </div>
}

function Parent({children}) {
  const [state, setState] = useState(0);

  return <div>
    <button onClick={x => x+1)>click</button>
    <Child02 />
    {children}
  </div>
}

when clicking on the button, you will get the following:

- button click
- setState(...), add Parent to dirty list
- start re-rendering all dirty nodes
- Parent rerenders
- Child02 rerenders
- DONE

Note that

  • Parent ( app ) and sibling ( Child03 ) nodes will not get re-rendered, or you'll end up with a re-render recursion.
  • Parent is re-rendered because its state has changed, so its output has to be recalculated.
  • {children} have not been affected by this change, so it stays the same. (unless a context is involved, but that's a different mechanism).
  • finally, <Child02 /> has been marked dirty, because that part of the virtual dom has been touched. While it's trivial for us to see it was not effected, the only way React could verify it is by comparing props, which is not done by default!
  • the only way to prevent Child02 from rendering is wrapping it with React.memo , which might be slower than just re-rendring it.

I created a codesandbox to dig in

Here is the codesandbox I created

and here's a short recording of me opening the debugger and seeing the call stack.

How it works

Starting from where you left off, Component.updateComponent:

  updateComponent(prevElement, nextElement) {
  //...
    if (shouldUpdateComponent(prevRenderedElement, nextRenderedElement)) {
      Reconciler.receiveComponent(this._renderedComponent, nextRenderedElement);
  //...

in the Component.updateComponent method Reconciler.receiveComponent is called which calls component.receiveComponent(element);

Now, this component refers to this._renderedComponent and is not an instance of Component but of DOMComponentWrapper

and here's the receiveComponent method of DOMComponentWrapper :

  receiveComponent(nextElement) {
    this.updateComponent(this._currentElement, nextElement);
  }

  updateComponent(prevElement, nextElement) {
    // debugger;
    this._currentElement = nextElement;
    this._updateDOMProperties(prevElement.props, nextElement.props);
    this._updateDOMChildren(prevElement.props, nextElement.props);
  }

Then _updateDOMChildren ends up calling the children render method.

here's a call stack from the codesandbox I created to dig in.

从 setState 调用堆栈直到子渲染

How do we end up in DOMComponentWrapper

in the Component 's mountComponent method we have:

let renderedComponent = instantiateComponent(renderedElement);
this._renderedComponent = renderedComponent;

and in instantiateComponent we have:

  let type = element.type;

  let wrapperInstance;
  if (typeof type === 'string') {
    wrapperInstance = HostComponent.construct(element);
  } else if (typeof type === 'function') {
    wrapperInstance = new element.type(element.props);
    wrapperInstance._construct(element);
  } else if (typeof element === 'string' || typeof element === 'number') {
    wrapperInstance = HostComponent.constructTextComponent(element);
  }

  return wrapperInstance;

HostComponent is being injected with DOMComponentWrapper in dilithium.js main file:

HostComponent.inject(DOMComponentWrapper);

HostComponent is only a kind of proxy meant to invert control and allow different Hosts in React.

here's the inject method:

function inject(impl) {
  implementation = impl;
}

and the construct method:

function construct(element) {
  assert(implementation);

  return new implementation(element);
}

Another answer might be the structure of the Fiber tree. During execution, react renders a ReactComponent into an object made out of ReactNode s and props. These ReactNode s are assembled into a FiberNode tree (which might be the in memory representation of the virutal dom?).

In the FiberNode tree, depending on the traversal algorithm (children first, sibling first, etc), React always has a single "next" node to continue. So, React will dive deeper into the tree, and update FiberNode s, as it goes along.

If we take the same example,

function App() {
  return <div>
    <Parent>
      <Child01/>
      <Child01/>
    </Parent>
    <Child03/>
  </div>
}

function Parent({children}) {
  const [state, setState] = useState(0);

  return <div>
    <button onClick={x => x+1)>click</button>
    <Child02 />
    {children}
  </div>
}

Which React will transform into this FiberNode tree:

node01 = { type: App, return: null, child: node02, sibling: null }
node02 = { type: 'div', return: node01, child: node03, sibling: null }
node03 = { type: Parent, return: node02, child: node05(?), sibling: node04 }
node04 = { type: Child03, return: node02, child: null, sibling: null }
node05 = { type: Child01, return: node03, child: null, sibling: node06 }
node06 = { type: Child01, return: node03, child: null, sibling: null }

// Parent will spawn its own FiberTree,
node10 = { type: 'div', return: node02, child: node11, sibling: null }
node11 = { type: 'button', return: node10, child: null, sibling: node12 }
node12 = { type: Child02, return: node10, child: null, sibling: node05 }

I might have missed something (ie. node03's child might be node10), but the idea is this - React always have a single node (the 'next' node) to render when it traverses the fiber tree.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM