React 如何在状态更改后更新组件及其子组件?

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

我在看Paul O Shannessy - Building React From Scratch

我非常了解挂载过程,但我很难理解 React 如何更新组件及其子组件

调节器通过这种方式控制更新过程:

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

  component.receiveComponent(element);
}

Component.receiveComponent

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

这是 Component.updateComponent 方法:

  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);
    } 
  }

这是在状态更改后更新组件的代码部分,我认为它也应该更新子组件,但我不明白这段代码是如何实现的,在安装过程中 React 实例化组件深入树中,但这不会发生在这里,我们需要找到第一个 HTML 元素然后我们可以改变我们的策略并在代码的另一个地方更新那个 HTML 元素,我无法通过这种方式找到任何 HTML 元素。

找到第一个 HTML 是停止这种无休止递归的方法,从逻辑上讲,这是我对代码的期望,在安装过程中以相同的方式停止递归,但在安装过程中,这个需要的组件实例化,因此我们可以委托给协调器,协调器会发现我们正在处理 HTML 元素的包装器实例,而不是自定义组件的包装器实例,然后 React 可以将该 HTML 元素放在 DOM.

我无法理解代码在更新过程中的工作原理。我看到的这段代码不会深入树中,我认为不会更新子元素,也不能让 React 找到第一个 HTML 元素,因此 React 可以更新 DOM 元素,不是吗?

这是 Github

上的代码库

我认为 React 不是 re-render parent 组件优先而不是 React re-render child 组件优先。

示例:A (parent) -> B (child) -> C (child of B) 当 A 更新状态 C (re-render) -> B -> A

React 完全复制实际的 DOM 并在 javascript 中创建虚拟的 DOM。在我们的应用程序中,每当我们更新任何最终在我们的组件中呈现的数据时,React 不会重新呈现整个 DOM。它只会影响重要的事情。所以反应实际上再次复制了虚拟 DOM 。这次它将更改应用于已更新的数据。

它将在红色组件中进行更改,然后将此虚拟 DOM 与旧 DOM 进行比较。它会看到不同的部分。然后它将仅将 DOM 更改应用于该不同的组件。

如果道具或状态发生变化,则更新阶段开始。如果顶层的数据发生变化:

如果它将数据向下传递给它的 children,所有 children 都将被重新渲染。如果 mid-level 处的组件状态发生变化:

这次只有它的 children 会被重新渲染。 React 将重新渲染该节点下树的任何部分。因为生成 children 组件视图的数据实际上位于 parent 组件(mid-level 一个)。但在它之上的任何东西,parent 或兄弟姐妹都不会重新渲染。因为数据不会影响他们。这个概念叫做Unidirectional Data Flow.

您可以在 chrome 浏览器中看到实际效果。选择渲染,然后启用 painting flushing 选项

如果您在页面上进行任何更改,您将看到更新的组件将闪烁。

更新阶段

componentWillReceiveProps 方法在组件生命周期的更新阶段首先被调用。当组件从其 parent 组件接收到新属性时调用它。使用此方法,我们使用 this.props object 将当前组件的属性与下一个组件的属性进行比较 使用 nextElement.props object。基于这种比较,我们可以选择使用 this.setState() 函数来更新组件的状态,该函数不会触发 此场景中的额外渲染。

请注意,无论您在 componentWillReceiveProps() 方法中调用 this.setState() 多少次,都不会触发该组件的任何额外渲染。 React 进行内部优化,将状态更新批处理在一起。

shouldComponentUpdated 指示组件是否应该重新渲染。默认情况下,所有 class 组件都会在收到的道具或状态发生变化时重新渲染。此方法可以通过 returning False 来防止默认行为。在这个方法中,现有的道具和状态值与下一个道具和状态值以及 return 布尔值进行比较,让 React 知道组件是否应该更新。此方法用于性能优化。如果它 returns False componentWillUpdate()render()componentDidUpdate() 不会被调用。

componentWillUpdate() 方法在 React 更新 DOM 之前立即调用。它有两个参数:nextPropsnextState。您可以使用这些参数为 DOM 更新做准备。但是,您不能在 componentWillUpdate() 方法中使用 this.setState()

调用 componentWillUpdate() 方法后,React 调用 render() 方法执行 DOM 更新。然后,调用 componentDidUpdate() 方法。

React 更新 DOM 后立即调用 componentDidUpdate() 方法。它得到这两个参数:prevPropsprevState。我们使用此方法与更新的 DOM 交互或执行任何 post-render 操作。例如,在计数器示例中,计数器编号在 componentDidUpdate.

中增加

调用componentDidUpdate()后,更新周期结束。当更新组件的状态或 parent 组件传递新属性时,将开始新的循环。或者当你调用 forceUpdate() 方法时,它触发了一个新的更新周期,但跳过了组件上的 shouldComponentUpdate() 方法(此方法用于优化) 触发了更新。但是,根据通常的更新阶段,shouldComponentUpdate() 会在所有 child 组件上调用。尽量避免使用forceUpdate()方式;这将提高您的应用程序的可维护性

嘿考虑根据需要使用 Tree 数据结构,ReactJs 遵循单向更新状态的方式,即一旦父状态发生变化,所有通过驻留在父组件中的道具传递的子组件将一劳永逸地更新! 考虑使用称为 深度优先搜索 的算法选项,它将为您找到连接到父节点的节点,一旦到达该节点,您将检查状态以及是否存在与父级共享的状态变量的偏差,您可以更新它们!

注意:这可能看起来有点理论化,但如果你能做一些远程接近这件事的事情,你就会创建一种更新组件的方法,就像反应一样!

我通过实验发现,React 只会在必要时重新渲染元素,这总是,除了 {children}React.memo()

正确使用子项,再加上批量 dom 更新,可以获得非常高效和流畅的用户体验。

考虑这个案例:

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>
}

当点击按钮时,您将得到以下内容:

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

注意

  • 父节点 (app) 和兄弟节点 (Child03) 将不会重新渲染,否则您将以重新渲染递归结束。
  • Parent重新渲染,因为它的状态已经改变,所以它的输出必须重新计算。
  • {children} 不受此更改的影响,因此保持不变。 (除非涉及上下文,但这是不同的机制)。
  • 最后,<Child02 /> 被标记为脏,因为虚拟 dom 的那部分已被触及。虽然我们很容易看到它没有受到影响,但 React 可以验证它的唯一方法是通过比较道具,默认情况下不会这样做!
  • 防止 Child02 渲染的唯一方法是用 React.memo 包装它,这可能比重新渲染它要慢。

我创建了一个 codesandbox 来挖掘

这里是the codesandbox I created

这是我打开调试器并查看调用堆栈的简短 recording

工作原理

从你离开的地方开始,Component.updateComponent:

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

Component.updateComponent 方法中调用 Reconciler.receiveComponent 调用 component.receiveComponent(element);

现在,这个 component 引用 this._renderedComponent 并且不是 Component 的实例,而是 DOMComponentWrapper

的实例

这里是 DOMComponentWrapper 的 receiveComponent 方法:

  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);
  }

然后 _updateDOMChildren 最终调用子 render 方法。

这是我创建的用于挖掘的 codesandbox 的调用堆栈。

我们如何在 DOMComponentWrapper 中结束

ComponentmountComponent 方法中我们有:

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

instantiateComponent中我们有:

  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 正在 dilithium.js 主文件中注入 DOMComponentWrapper

HostComponent.inject(DOMComponentWrapper);

HostComponent 只是一种代理,旨在反转控制并允许在 React 中使用不同的 Host。

这是 inject 方法:

function inject(impl) {
  implementation = impl;
}

construct方法:

function construct(element) {
  assert(implementation);

  return new implementation(element);
}

当我们没有 DOMComponentWrapper 时

如果我们正在更新非主机组件链,例如:

const Child = <div>Hello</div>

const Parent = () => <Child />

Child 如何从更新到 Parent 得到渲染?

Parent Component 有以下内容:

  • _renderedComponent 是 Child 的实例(也是 Component
    • renderedComponent 有一个 Child 实例,因为它获取“根”元素的类型(由 render 方法返回的元素)

所以 Reconciler.receiveComponent(this._renderedComponent, nextRenderedElement) 将调用 Child 的 component.receiveComponent(element),后者又调用 this.updateComponent(this._currentElement, nextElement);(属于 Child),后者调用它的 render 方法(let nextRenderedElement = this.render();)

另一个答案可能是 Fiber 树的结构。在执行期间,React 将 ReactComponent 渲染为由 ReactNode 和 props 组成的 object。这些 ReactNode 被组装成一棵 FiberNode 树(这可能是虚拟 dom 的内存表示?)。

FiberNode 树中,根据遍历算法(children 优先,sibling 优先等),React 总是有一个“下一个”节点可以继续。因此,React 将深入到树中,并随着它的进行更新 FiberNodes。

如果我们举同样的例子,

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>
}

React 将转换成这个 FiberNode 树:

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 }

我可能漏掉了一些东西(即 node03 的 child 可能是 node10),但想法是这样的——React 在遍历时总是有一个节点('next' 节点)来渲染纤维树。