深入浅出setState

65 阅读3分钟

读完本文你讲知道:

  • setState 不会立即改变 React 组件中 state 的值 (异步)
    • setState异步的,包括在setTimeout里也是异步的 (很多文章说在 setTimeout 里是同步的,我这里用 react18 测试依然是异步)
  • setState 通过引发一次组件的更新过程来引发重新绘制
  • 多次 setState 函数调用产生的效果会合并(批处理)

setState 的特性——批处理

如果在同一周期多次调用 setState ,后调用的 setState 将覆盖先调用的 setState 的值,例如:

// state.count === 0
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
this.setState({count: state.count + 1});
// state.count === 1

执行 3+1,但最后只加了 1 次;若在 setTimeout 中多次调用,结果也一样

// state.count === 0
setTimeout(() => {
    this.setState({count: state.count + 1});
    this.setState({count: state.count + 1});
    this.setState({count: state.count + 1});
}, 0)
// state.count === 1

因为这样的操作相当于 Object.assign,最后一个会把前面的都给覆盖

// 相当于
Object.assign(
  state,
  {count: state.count + 1},
  {count: state.count + 1},
  {count: state.count + 1},
)

同一个时期,多次调用,会合并

函数组件类组件在同一时期,多次调用setState,会合并。

const DemoState = (props) => {
  let [number, setNumber] = useState(0);

  const add = () => {
    setNumber(number+1);
    console.log(number); // 0

    setNumber(number+1);
    console.log(number); // 0

    setNumber(number+1);
    console.log(number); // 0
  }

  return (
    <div>
        <span>{ number }</span>
        <button onClick={() => { add() }} >点击加 1</button>
    </div>
  )
}

// 0
// 0
// 0

// 页面展示 number 为 1

若上面的 3+1 都放在 setTimeout 执行,也是会合并的,并且仍然是异步 (很多文章说在 setTimeout 里是同步的,我这里用 react18 测试依然是异步)

const DemoState = (props) => {
  let [number, setNumber] = useState(0);

  const add = () => {

    setTimeout(() => {
      setNumber(number+1);
      console.log(number); // 0

      setNumber(number+1);
      console.log(number); // 0

      setNumber(number+1);
      console.log(number); // 0
      }, 0)
  }
  return (<div>
      <span>{ number }</span>
      <button onClick={() => { add() }} >点击加 1</button>
  </div>)
}
// 0
// 0
// 0

// 页面展示 number 为 1

函数组件在不同时期,会合并

函数组件多次调用 +1 操作,分别在不用时期:一个在 setTimeout 调用,另一个在 setTimeout 调用。最后合并了,只调用了 1 次 +1

const DemoState = (props) => {
  let [number, setNumber] = useState(0);

  const add = () => {
    setNumber(number+1);
    console.log(number); // 0

    setTimeout(() => {
      setNumber(number+1);
      console.log(number); // 0
      }, 0)
  }
  return (<div>
      <span>{ number }</span>
      <button onClick={() => { add() }} >点击加 1</button>
  </div>)
}

// 0
// 0 

// 页面展示 number 为 1

类组件在不同时期,不会合并

类组件多次调用 +1 操作,分别在不用时期:一个在 setTimeout 调用,另一个在 setTimeout 调用。最后没合并2 次 +1 都被调用

class DemoState2 extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 0
    }
  }

  add = () => {
    this.setState({number: this.state.number + 1});
    console.log(this.state.number); // 0

    setTimeout(() => {
      this.setState({number: this.state.number + 1});
      console.log(this.state.number); // 1
    }, 0)
  }

  render() {
    return (
      <div>
        <span>{ this.state.number }</span>
        <button onClick={this.add} >点击加 1</button>
      </div>
    )
  }
}

// 0
// 1 

// 页面展示 number 为 2

当前测试 react 版本:18.1.0

这可能是 react 的一个 bug,看看后面会不会在函数组件和类组件中保持一致。

下面看看由批处理引发的问题:

问题 1:连续使用 setState,为什么不能实时改变

state.count = 0;
this.setState({count: state.count + 1}); 
this.setState({count: state.count + 1}); 
this.setState({count: state.count + 1}); 
// state.count === 1,不是 3

因为 this.setState 方法为会进行批处理,后调的 setState 会覆盖统一周期内先调用的 setState 的值,如下所示:

state.count = 0;
this.setState({count: state.count + 2}); 
this.setState({count: state.count + 3}); 
this.setState({count: state.count + 4}); 
// state.count === 4

问题 2:为什么要 setState,而不是直接 this.state.xx = oo?

  1. setState 不仅仅修改了 this.state 的值,更重要的是它会触发 React更新机制,会进行 diff,然后将 patch 部分更新到真实 dom
  2. 如果直接 this.state.xx = oo 的话,state 的值确实会改,但是它不会驱动 React 重渲染,不会触发后续生命周期,如 shouldComponentUpdaterender 等一系列函数的调用。
  3. 对于批处理,多次setState 只产生一次重新渲染,将对 Virtual DOMDOM 树操作降到最小,用于提高性能

问题 3:那为什么会出现异步的情况呢?(为什么这么设计?)

因为性能优化。假如每次 setState 都要更新数据,更新过程就要走五个生命周期,走完一轮生命周期再拿 render 函数的结果去做 diff 对比和更新真实 DOM,会很耗时间。所以将每次调用都放一起做一次性处理,能降低对 DOM 的操作,提高应用性能

问题 4:那如何在表现出异步的函数里可以准确拿到更新后的 state 呢?

  • setState(stateChange[, callback])
  • setState((state, props) => stateChange[, callback])
onHandleClick() {
  this.setState(
    {count: this.state.count + 1,},
    () => {
      console.log("点击之后的回调", this.state.count); // 最新值
    }
  );
}

this.setState(state => {
 console.log("函数模式", state.count);
 return { count: state.count + 1 };
});