React 如何在重新渲染父组件时重用子组件/保持子组件的状态?

lua*_*wtf 6 javascript reactjs

在 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'));
Run Code Online (Sandbox Code Playgroud)

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

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

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

Bud*_*nWA 5

createElementVS renderVS 坐骑

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

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

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

React 的 diff 算法

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

React 比较子元素的默认方法是同时迭代两个子元素列表,将第一个元素相互比较,然后比较第二个元素,依此类推。

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

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

例子

Button总是返回一个元素: a button​​。因此,当您Button重新渲染、button匹配以及重新使用其 DOM 元素时,就会button比较 的子元素。

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

第二个子元素始终是字符串"Clicks: ",因此 React 也会单独保留该 DOM 元素。

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


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

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

然后在第一步中,a将与 the 进行比较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>