React类组件setState失效

781 阅读5分钟

问题背景

有一天,我正在座位上摸鱼(划掉),组长突然跑过来跟我说,帮看一个问题,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现象以及断点调试情况

image-20230520111158805

代码执行到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重新执行?

  1. 组件初始化时:当组件在挂载阶段时,constructor 方法会被调用一次,用于初始化组件的状态和绑定方法等操作。
  2. 组件更新时:当组件的 propsstate 发生变化时,会触发组件的重新渲染。如果新的 propsstate 值与之前的值不同,那么 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 不会被更新。