setState 更新状态行为理解「useState 同理」

1,333 阅读3分钟

更新状态 setState

react 类式组件更新页面可以通过组件实例访问 this.setState(stateChange, callback) 方法。

  1. setState 函数,可以接收两个参数,作用:更新状态,重新调用当前组件的 render 渲染
    • 第一个参数:为对象或者函数,在框架底层将此对象本身或者函数调用后的返回值与当前状态,进行类似 Object.assign()合并操作
      // 对象
      this.setState({
        counter: this.state.counter + this.props.increment,
      })
      
      // 函数
      this.setState((preState, props) => ({
        counter: preState.counter + props.increment
      }))
      
    • 第二个参数:为回调函数,在状态数据合并操作完毕后,调用组件的 render 渲染函数之前,才执行第二个参数回调。
      render() {
          let { count } = this.state
          return <div>
              <button onClick={() => {
                  this.setState({ count: count + 1 }, () => {
                      console.log('改变之前 count', count);
                      console.log('改变之后 count', this.state.count);
                  })
              }}>点我加1</button>
              当前计数器为{count}
          </div>
      }
      
  2. setState() 这一句调用代码本身是同步执行,但是【合并操作】与 【调用 render 渲染函数】这两个底层行为都是异步执行的(微任务)。所以要小心,在同步执行的代码中取值行为,如下代码,两次取值之后,合并操作与重新渲染组件才会执行,所以两次取值都是相同的。
    state = { count: 0 }
    
    // ❌ 第二次调用,this.state.count 值与第一次调用取值是一样的,都是 0 + 1
    handle = () => {
        this.setState({ count: this.state.count + 1 }, () => {   
            ... 
        })
        this.setState({ count: this.state.count + 1 }, () => {
            ... 
        })
    }
    
    // ✅ 将 setState 的第一个参数写为函数即可收到每次更新的值
    handle = () => {
        this.setState(preState => ({ count: preState.count + 1 }), () => {
            ...
        })
        this.setState(preState => ({ count: preState.count + 1 }), () => {
            ...
        })
    }
    

setState 执行流程

没看过源码或分析源码文章求证,纯自测,符合测试结果

  1. 当 setState 函数第一个参数为对象时:
    1. 执行同步代码: setState 函数调用,第一个参数对象成员取值,注册下面的 3 个异步微任务,继续执行后面的同步代码
    2. 清空(执行)当前宏任务中的微任务:
      1. 执行合并操作任务,将第一个参数对象与当前 state 对象合并(状态数据更新了)
      2. 执行 setState 的第二个参数回调
      3. 执行当前组件的 render 函数(视图更新了)
  2. 当 setState 函数第一个参数为函数时:
    1. 执行同步代码: setState 函数调用,第一个参数函数定义,注册下面的 4 个异步微任务,继续执行后面的同步代码
    2. 清空(执行)当前宏任务中的微任务:
      1. 执行第一个参数函数,拿到返回值对象,将对象成员取值
      2. 执行合并操作任务,将返回值对象与当前 state 对象合并(状态数据更新了)
      3. 执行 setState 的第二个参数回调
      4. 执行当前组件的 render 函数(视图更新了)

useState Hook 中的 setXXX 与 class 组件的 setState 函数执行流程一样,唯二区别是:1. setXXX 没有重载,只有一个参数,没有第二个参数回调。2. setXXX 更新状态是替换操作,并非合并操作

多次调用合并渲染

当多次调用 setState 函数时,react 做了性能优化。

  1. 同步代码环境中调用 setState:在正常的 react 的事件流里,一个类式组件多次执行 setState 或者函数组件某个 useState 中的 setXXX 多次执行,只会调用一次重新渲染 render,称为合并渲染

    如果没有合并渲染,在每次执行 setState 更新函数时,组件都要重新 render 一次,会造成无效渲染,浪费时间(因为最后一次渲染会覆盖掉前面所有的渲染效果)

    所以 react 会把一些可以一起更新的 setState 放在一起,进行合并,只渲染最后一次。

  2. 异步代码环境中调用 setState:在 setTimeout,Promise.then 等异步事件中,多次执行 setState 和 useState 中的 setXXX,每次执行都会调用 render 渲染函数

    setTimeout 已经超出了 react 的控制范围,react 无法对 setTimeout 的回调代码前后加上事务逻辑(除非 react 重写 setTimeout)。

    当遇到 setTimeout/setInterval/Promise.then(fn)/fetch 回调/xhr 网络回调 时,react 都是无法控制的。