繁体   English   中英

React 在重新渲染父组件时如何重用子组件/保留子组件的 state?

[英]How does React re-use child components / keep the state of child components when re-rendering the parent component?

在 React 中,每次渲染/重新渲染组件时,它都会使用createElement重新生成它的所有子节点/组件。 React 如何知道何时在重新渲染之间保留组件 state?

例如,考虑以下代码:

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组件的 state 保持不变,不会被替换。 React 如何知道重用组件实例?

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

createElement vs render vs 挂载

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

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

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

React 的 diffing 算法

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

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

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

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

例子

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

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

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

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


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

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

然后在第一步中,将abutton进行比较,因为它们是不同的类型,旧元素及其所有子元素将从 DOM 中删除、卸载和销毁。 然后新元素将在没有先前渲染结果的情况下创建,因此将使用新的 state 创建一个新的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>

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

即使子组件的道具已更改,子组件的 state 是否仍然存在? (在 Vue 中,组件的 state 在其 props 更改时确实存在)

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

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

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

由于返回值取决于本地 state,而不是道具,因此当道具更改时,此子项不会重新渲染。

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

当道具更改时,这个孩子将重新渲染,因为本地 state 使用新道具更新。

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 节点,但组件的 state 仍然存在。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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