javascript - 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 节点,但组件的状态仍然存在。
解决方案
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
总是只返回一个元素: a button
。因此,当您Button
重新渲染时,button
匹配项及其 DOM 元素被重新使用,然后将button
比较 的子项。
第一个孩子总是 a Timer
,所以类型匹配并且组件实例被重用。Timer
props 没有改变,所以 React 可能会重新渲染它(调用具有render
相同状态的实例),或者它可能不会重新渲染它,从而保持树的那一部分保持不变。这两种情况都会在你的情况下产生相同的结果——因为你没有副作用render
——并且 React 故意将何时重新渲染的决定作为实现细节留下。
第二个孩子总是字符串"Clicks: "
,所以 react 也只留下那个 DOM 元素。
如果this.state.click
自上次渲染后发生了变化,那么第三个孩子将是一个不同的字符串,可能从"0"
变为 to "1"
,因此文本节点将在 DOM 中被替换。
如果Button
srender
要返回不同类型的根元素,如下所示:
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> |
推荐阅读
- android - 如果应用程序包含两个服务,这两个服务是否都在主线程上运行
- go - 如何将累加器传递给递归函数?
- python - 将地图转换为列表返回 TypeError:“int”对象不可调用
- android - 在 Fragment 上使用回收站视图时出错
- json - 在 Swift 中解析 json blob 字段
- c# - 根据第一列值从列表视图中删除重复项
- c# - 通过菜单按钮删除 RadGridView 中的行
- android - 清单文件问题
- oracle - 使用 oracle PLSQL Bulk collect with limit 子句将整个数据收集到集合中
- .net - 是否可以在支持 Linq 到 SQL 转换的同时扩展 String 类?