问题背景
有一天,我正在
座位上摸鱼(划掉),组长突然跑过来跟我说,帮看一个问题,setState失效了。我跑过去一看,代码执行到this.setState这一行就跳过了,并没有执行setState以及setState中回调的第二个参数。
这是什么原因导致的呢?
出现问题的代码
- 父组件
import React, { Component } from 'react'
import Son from './Son';
export default class index extends Component {
ref = null;
onRef = (ref) => {
this.ref = ref;
}
clickHandle = () => {
this.ref.show();
}
render() {
return (
<div>
<button onClick={this.clickHandle}> ++++ </button>
<Son
onRef={this.onRef}
/>
</div>
)
}
}
- 子组件
import React, { Component } from 'react'
export default class Son extends Component {
constructor(props){
super(props);
this.state = {
num: 1,
}
}
static getDerivedStateFromProps(nextProps, prevState) {
new Son(nextProps).setRef();
return null;
}
setRef = () => {
if(this.props.onRef){
this.props.onRef(this);
}
}
show = () => {
this.setState({
num: 2
}, () => console.log(this.state.num))
}
render() {
return (
<div>Son</div>
)
}
}
Bug现象以及断点调试情况
代码执行到27行,直接就跳到30行了,并没有执行setState,但是控制台也没有报错
猜测以及验证
- 猜测1
- 当父组件调用子组件的方法的时候,construtor重新执行,导致this指针指向错误
猜测执行顺序
1. 调用子组件函数 ->
2. 不知道什么原因导致了父组件更新 ->
3. 子组件的constructor重新执行,注意,子组件不会重新执行componentDidMount,但是会重新执行render函数 ->
4. 子组件的this指向了重新创建的组件实例对象 ->
5. this.setState失效
但是通过查阅资料发现:
constructor 方法会重新执行,但是 setState 方法不会被中断或停止执行。
如果在 setState 方法执行期间发生了组件的重新实例化,React 会创建一个新的组件实例来代替旧的组件实例,以完成组件的更新。在新的组件实例中,constructor 方法会被重新执行,但是 setState 方法不会停止执行,并且新的状态更新也会被应用到新的组件实例中。
虽然 constructor 方法可能会被多次调用,但是组件的实例只会被创建一次。这意味着在 constructor 方法中创建的任何对象或变量都会被同一个组件实例共享。
总结:虽然constructor会重新执行,但是组件的实例只会被创建一次。
除非 setState 方法执行期间发生了组件的重新实例化
- 猜测2
getDerivedStateFromProps中执行了new Son(nextProps)的操作,导致this指向错误
验证
// 注释getDerivedStateFromProps这段代码,改为在componentDidMount函数周期中回传子组件的this
componentDidMount () {
this.setRef();
}
结果:成功执行this.setState,且不会重新执行constructor
总结和反思
总结
如果你在父组件中通过new关键字创建了一个新的子组件实例,那么这个新的子组件实例是与之前的子组件实例不同的。尽管新的子组件实例与之前的子组件实例具有相同的代码和方法,但是它们是不同的对象,拥有不同的状态和属性。
因此,当你在父组件中通过this.ref.show();调用新的子组件实例的方法时,这个子组件的show方法中调用的this.setState()实际上是更新新的子组件实例的状态,而不是之前的子组件实例的状态。因此,即使新的子组件实例的状态发生了变化,但由于之前的子组件实例已经被卸载并从DOM中删除,所以它不会对UI界面造成任何影响。
总之,当你在父组件中通过this.ref.show()调用子组件的方法时,这个方法中调用的this.setState()会自动触发组件的重新渲染流程,从而更新UI界面。但是,如果你在父组件中通过new关键字创建了一个新的子组件实例,那么新的子组件实例与之前的子组件实例是不同的对象,它们拥有不同的状态和属性。因此,新的子组件实例中的this.setState()不会影响之前的子组件实例的状态。
反思
1. 什么情况会导致contructor重新执行?
- 组件初始化时:当组件在挂载阶段时,
constructor方法会被调用一次,用于初始化组件的状态和绑定方法等操作。 - 组件更新时:当组件的
props或state发生变化时,会触发组件的重新渲染。如果新的props或state值与之前的值不同,那么 React 会执行一次更新操作。在更新操作中,如果组件是基于类的,那么它的constructor方法也会被调用一次。
2. setState 方法执行期间发生了组件的重新实例化会导致虽然this打印出来是当前对象,但并不是当前的实例,会导致setState失败
3.其他零碎的收获
- 函数执行componentDidMount的条件:
- 组件被挂载 --
<Component /> - 组件被实例化 -- constructor
- 组件被渲染 -- render
- 组件被挂载 --
- 当props发生改变的时候,会执行render,但是不会重新执行constructor
constructor方法是在组件实例化时被调用,用于初始化组件的状态和绑定方法等操作。constructor方法是在组件实例化时被调用,它在componentDidMount之前执行。- 当
constructor方法被重新执行时,它的上下文环境会发生变化,因此this对象也会发生变化。 constructor方法和render方法的主要区别在于它们的目的和职责不同。constructor是用于初始化组件的,而render则是用于生成组件的 UI 表示的。- 即使
render方法被调用,也不一定会导致真正的 DOM 更新。React 使用 Virtual DOM 来管理组件的 UI 表示,它会在每次组件更新时比较前后两个 Virtual DOM 树的差异,并只更新需要更新的部分。因此,即使render方法被调用,如果生成的 Virtual DOM 树与之前的树相同,那么真正的 DOM 不会被更新。