setState小总结

650 阅读3分钟

如何获取setState后的值

方法一

class Demo extends Component {
    state = {
    isActive: false
  }

  // 对象更新
  handleClick1(){
    this.setState({
      isActive: true
    }, () => {
      console.log(this.state.isActive) // true
    })
     console.log(this.state.isActive) // false
  }
    
    // 回调函数更新,当我们的数据需要以来之前的state,和props则采用这种方式
  handleClick2(){
    this.setState((prevState, props) => {
      isActive: !prevState.isActive
    }, () => {
      console.log(this.state.isActive) // true -> 第一次点击
    })
     console.log(this.state.isActive) // false -> 第一次点击
  }

    render(){
    return(
        <>
        <button onClick={handleClick1}></button>
      </>
    )
  }
}

方法二

class Demo extends Component {
    state = {
    isActive: false
  }

  async handleClick(){
    await this.setState({
      isActive: true
    })
    console.log(this.state.isActive) // true
  }

    render(){
    return(
        <>
        <button onClick={handleClick}></button>
      </>
    )
  }
}

为什么不能用 this.state.XXX=新值 的方式更新数据?

当执行setState时,会将需要更新的state合并后放入状态队列,而不会立即更新this.state,队列机制可以高效地批量更新state。如果不通过setState方式而直接用 this.state.XXX=新值 的方式,那么改state将不会放入状态队列当中,当下次调用setState并对状态队列进行合并时,将会忽略之前直接被修改的state,而造成无法预知的错误。

setState的更新机制

先来段代码

import React, { Component } from 'react';

class Counter extends Component{
    state = {count : 0} 
    
    incrementCount = () => {
        this.setState({count : this.state.count + 1}) 
        this.setState({count : this.state.count + 1})
    }
    render(){
        return <div>
                <button onClick={this.incrementCount}>Increment</button>
                <div>{this.state.count}</div>
            </div>
    }
}

export default Counter;

切入主题

image.png

当setState被调用时,新的state会进入一个状态队列,相同的操作被合并,这里的合并可以理解成是React做了一次浅拷贝合成的新值,

Object.assign(
    previousState,
    {count : state.count + 1},
    {count : state.count + 1}
)

之后调用enqueueUpdate方法决定是否采用批量更新方式更新组件,判断条件是isBatchingUpdates。

setState究竟是异步的还是同步的

核心点

setState 并非真异步,只是看上去像异步。在源码中,通过 isBatchingUpdates 来判断

setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。

场景

在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。

但在 React 无法控制的地方,比如原生事件,具体就是在 addEventListener、setTimeout、setInterval 等事件中,就只能同步更新。

那么为什么采用异步的操作?

1. 一般认为,做异步设计是为了性能优化、减少渲染次数

2. 保持内部一致性。如果将 state 改为同步更新,那尽管 state 的更新是同步的,但是 props不是。

首先,我想我们都同意推迟并批量处理重渲染是有益而且对性能优化很重要的,无论 setState() 是同步的还是异步的。那么就算让 state 同步更新,props 也不行,因为当父组件重渲染(re-render )了你才知道 props

假设 state 是同步更新的,那么下面的代码是可以按预期工作的:

console.log(this.state.value) // 0
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 1
this.setState({ value: this.state.value + 1 });
console.log(this.state.value) // 2
复制代码

然而,这时你需要将状态提升到父组件,以供多个兄弟组件共享:

-this.setState({ value: this.state.value + 1 });
+this.props.onIncrement(); // 在父组件中做同样的事
复制代码

需要指出的是,在 React 应用中这是一个很常见的重构,几乎每天都会发生。

然而下面的代码却不能按预期工作:

console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
this.props.onIncrement();
console.log(this.props.value) // 0
复制代码

这是因为同步模型中,虽然 this.state 会立即更新,但是 this.props 并不会。而且在没有重渲染父组件的情况下,我们不能立即更新 this.props。如果要立即更新 this.props (也就是立即重渲染父组件),就必须放弃批处理(根据情况的不同,性能可能会有显著的下降)。

所以为了解决这样的问题,在 React 中 this.statethis.props 都是异步更新的,在上面的例子中重构前跟重构后都会打印出 0。这会让状态提升更安全。

最后 React 模型更愿意保证内部的一致性和状态提升的安全性,而不总是追求代码的简洁性。

3. 启用并发更新,完成异步渲染。