首页 > 解决方案 > React 在重新渲染父组件时如何重用子组件/保持子组件的状态?

问题描述

在 React 中,每次渲染/重新渲染组件时,它都会使用createElement. React 如何知道何时在重新渲染之间保持组件状态?

例如,考虑以下代码:

class Timer extends Component {
  constructor(props) {
    super(props);
    this.state = { seconds: 0 };
  }
  tick() {
    this.setState(state => ({ seconds: state.seconds + 1 }));
  }
  componentDidMount() {
    this.interval = setInterval(() => this.tick(), 1000);
  }
  componentWillUnmount() {
    clearInterval(this.interval);
  }
  render() {
    return createElement('div', null,
      'Seconds: ',
      this.state.seconds
    );
  }
}
class Button extends Component {
  constructor(props) {
    super(props);
    this.state = { clicks: 0 };
  }
  click() {
    this.setState(state => ({ clicks: state.clicks + 1 }));
  }
  render() {
    return createElement('button', { onClick: () => this.click() },
      createElement(Timer, null),
      'Clicks: ',
      this.state.clicks
    );
  }
}
render(createElement(Button, null), document.getElementById('root'));

您可以在此处使用 Preact REPL 尝试此代码。

请注意,当按下按钮并更新 clicks 值时,Timer组件的状态保持不变并且不会被替换。React 如何知道重用组件实例?

虽然一开始这似乎是一个简单的问题,但当您考虑更改传递给子组件的道具或子组件列表等事情时,它会变得更加复杂。React 如何处理更改子组件的 props?即使子组件的道具发生了变化,它的状态是否仍然存在?(在 Vue 中,组件的状态会在其 props 更改时保持不变)列表怎么样?当子组件列表中间的条目被删除时会发生什么?对这样的列表进行更改显然会生成非常不同的 VDOM 节点,但组件的状态仍然存在。

标签: javascriptreactjs

解决方案


createElementvs rendervs 安装

当像你这样的 React 组件Button被渲染时,会使用createElement. createElement(Timer, props, children)不会创建Timer组件的实例,甚至不会渲染它,它只会创建一个“React 元素”,它表示应该渲染组件的事实。

当你Button被渲染时,react 会将结果与之前的结果进行协调,以决定需要对每个子元素执行什么操作:

  • 如果元素与先前结果中的元素不匹配,则创建一个组件实例,然后安装然后渲染(递归地应用相同的过程)。请注意,当Button第一次渲染时,所有的孩子都将是新的(因为没有以前的结果可以匹配)。
  • 如果元素与上一个结果中的一个匹配,则重用组件实例:更新其 props,然后重新渲染组件(再次递归地应用相同的过程)。如果 props 没有改变,React 甚至可能选择不重新渲染以提高效率。
  • 先前结果中与新结果中的元素不匹配的任何元素都将被卸载并销毁。

React 的 diffing 算法

如果 React 比较它们并且它们具有相同的类型,则一个元素“匹配”另一个元素。

React 比较子元素的默认方式是简单地同时遍历两个子元素列表,将第一个元素相互比较,然后再比较第二个,等等。

如果孩子有keys,则将新列表中的每个孩子与旧列表中具有相同键的孩子进行比较。

有关更详细的说明,请参阅React Reconciliation Docs 。

例子

Button总是只返回一个元素: a button​​。因此,当您Button重新渲染时,button匹配项及其 DOM 元素被重新使用,然后将button比较 的子项。

第一个孩子总是 a Timer,所以类型匹配并且组件实例被重用。Timerprops 没有改变,所以 React 可能会重新渲染它(调用具有render相同状态的实例),或者它可能不会重新渲染它,从而保持树的那一部分保持不变。这两种情况都会在你的情况下产生相同的结果——因为你没有副作用render——并且 React 故意将何时重新渲染的决定作为实现细节留下。

第二个孩子总是字符串"Clicks: ",所以 react 也只留下那个 DOM 元素。

如果this.state.click自上次渲染后发生了变化,那么第三个孩子将是一个不同的字符串,可能从"0"变为 to "1",因此文本节点将在 DOM 中被替换。


如果Buttonsrender要返回不同类型的根元素,如下所示:

  render() {
    return createElement(this.state.clicks % 2 ? 'button' : 'a', { onClick: () => this.click() },
      createElement(Timer, null),
      'Clicks: ',
      this.state.clicks
    );
  }

然后在第一步中,a将 与 进行比较button,因为它们是不同的类型,旧元素及其所有子元素将从 DOM 中删除、卸载和销毁。然后新元素将在没有先前渲染结果的情况下创建,因此Timer将创建一个具有新状态的新实例,并且计时器将返回 0。


Timer火柴? 上一棵树 新树
不匹配 <div><Timer /></div> <span><Timer /></span>
匹配 <div>a <Timer /> a</div> <div>b <Timer /> b</div>
不匹配 <div><Timer /></div> <div>first <Timer /></div>
匹配 <div>{false}<Timer /></div> <div>first <Timer /></div>
匹配 <div><Timer key="t" /></div> <div>first <Timer key="t" /></div>

推荐阅读