【问题标题】:How does React re-use child components / keep the state of child components when re-rendering the parent component?React 在重新渲染父组件时如何重用子组件/保持子组件的状态?
【发布时间】:2021-04-04 09:51:50
【问题描述】:

在 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 here 尝试此代码。

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

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

【问题讨论】:

  • createElement 不会创建新组件。它返回一个描述(ReactElement)在该位置渲染哪个组件,具有哪些属性和子级。这是 react 用来确定组件树是否发生变化的。
  • 知道这一点很有用,我目前正在阅读 React 的源代码以更好地了解他们的 VDOM 实现是如何工作的。
  • 如果您发现任何关于他们的差异算法的见解(尤其是关于元素被认为“相同”与不同的情况),请确保post an answer yourself - 我也很好奇
  • “当一个组件更新时,实例保持不变,因此状态在渲染之间保持不变。” reactjs.org/docs/…
  • @FelixKling 我认为 OP 知道这一点,问题是 React 认为什么是更新,什么是新组件。

标签: javascript reactjs


【解决方案1】:

从未使用过 Vue,但这是我的看法。

即使它的 props 发生了变化,子组件的状态是否仍然存在? (在 Vue 中,组件的状态会在其 props 更改时保持不变)

这取决于你如何处理孩子身上的道具。

每次你改变(变异)你的道具时,这个孩子都会重新渲染。

const Child = (props) => {
    return <div>{ props.username }</div>;
};

当 props 改变时,这个孩子不会重新渲染,因为返回值取决于本地状态,而不是 props。

const Child = (props) => {
    const [state, setState] = useState(props.username);
    return <div>{ state }</div>;
};

当 props 改变时,这个子组件会重新渲染,因为本地 state 会随着新的 props 更新。

const Child = (props) => {
    const [state, setState] = useState(props.username);

    useEffect(() => {
        // changing props changes the component's state, causing a re-render
        setState(props.username); 
    }, [props]);

    return <div>{ state }</div>;
};

从上面的例子中可以看出,程序员是控制 React 是否触发子节点重新渲染的人。

列表呢?当子组件列表中间的条目被删除时会发生什么?对这样的列表进行更改显然会生成非常不同的 VDOM 节点,但组件的状态仍然存在。

当涉及到子组件列表时(例如,当使用 .map 时)React 将需要 key 参数,以便 React 知道在父组件重新渲染之间添加/删除/更改了什么。 React 要求对相同的组件使用相同的密钥,以防止不必要的重新渲染(不要使用 Math.random() 作为密钥)。

【讨论】:

  • "当 props 改变时,这个孩子不会重新渲染,因为返回值不依赖于 props" - 如何反应知道?我假设它仍然调用组件的 render 方法(或调用组件函数),即使它不会更新 DOM,因为 VDOM 保持不变。
  • 它根据状态改变。您可以在子状态中放置一个 console.log() 以查看它是否重新渲染。重新渲染时会调用console.log
【解决方案2】:

createElement vs render vs mount

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

当你的Button 被渲染时,react will reconcile 的结果和之前的结果,来决定每个子元素需要做什么:

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

React 的 diffing 算法

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

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

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

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

示例

您的Button 始终只返回一个元素:button。因此,当您的 Button 重新渲染时,button 匹配,并且其 DOM 元素被重用,然后比较 button 的子元素。

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

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

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


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

  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 matches? previous tree new tree
no match &lt;div&gt;&lt;Timer /&gt;&lt;/div&gt; &lt;span&gt;&lt;Timer /&gt;&lt;/span&gt;
match &lt;div&gt;a &lt;Timer /&gt; a&lt;/div&gt; &lt;div&gt;b &lt;Timer /&gt; b&lt;/div&gt;
no match &lt;div&gt;&lt;Timer /&gt;&lt;/div&gt; &lt;div&gt;first &lt;Timer /&gt;&lt;/div&gt;
match &lt;div&gt;{false}&lt;Timer /&gt;&lt;/div&gt; &lt;div&gt;first &lt;Timer /&gt;&lt;/div&gt;
match &lt;div&gt;&lt;Timer key="t" /&gt;&lt;/div&gt; &lt;div&gt;first &lt;Timer key="t" /&gt;&lt;/div&gt;

【讨论】:

    猜你喜欢
    • 2019-06-01
    • 1970-01-01
    • 2022-06-29
    • 2018-08-12
    • 1970-01-01
    • 2021-04-06
    • 2020-12-30
    • 2019-09-21
    • 2019-02-26
    相关资源
    最近更新 更多