由浅入深setState

背景

在平时的项目开发中,编写Class Component组件时,setState应该是必须会用到的,这篇文章就来由浅入深的总结下setState的相关知识。 首先思考下面代码

//.....
this.state.a = 0
//.....
handleClick = () => {
    for(let i = 0; i< 10; i++){
	this.setState({ a: i })
    }	
}
//....
//.....
this.state.a = 0
//.....
handleClick = () => {
    for(let i = 0; i< 10; i++){
	this.setState({ a: i })
    }	
}
//....

结果this.state.a的值是多少呢?

setState的用途

React中更新组件的三种方式分别是props的改变setState改变组件状态和forceUpdate方法。 父子组件之间的通信是通过props和回调,在组件内部的更新则是通过setState改变组件状态来触发。forceUpdate() 强制让组件重新渲染,将致使组件调用 render() 方法。

setState() 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式

setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

setState() 并不总是立即更新组件。它会批量推迟更新。这使得在调用 setState() 后立即读取 this.state 成为了隐患。为了消除隐患,请使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发。如需基于之前的 state 来设置当前的 state,请阅读下述关于参数 updater 的内容。

除非 shouldComponentUpdate() 返回 false,否则 setState() 将始终执行重新渲染操作。如果可变对象被使用,且无法在 shouldComponentUpdate() 中实现条件渲染,那么仅在新旧状态不一时调用 setState()可以避免不必要的重新渲染

setState的写法方式

对象方式

//...
constructor(props){
    super(props)
    this.state = {
	a: 0	
    }
}

componentDidMount(){
    this.setState({a: 1})
}
//...
//...
constructor(props){
    super(props)
    this.state = {
	a: 0	
    }
}

componentDidMount(){
    this.setState({a: 1})
}
//...

函数来作为参数

//....
constructor(props){
    super(props)
    this.state = {
	a: 0	
    }
}

componentDidMount(){
    this.setState((prevState,props)=>{
	a: prevState.a + 1 
    })
}
//....
constructor(props){
    super(props)
    this.state = {
	a: 0	
    }
}

componentDidMount(){
    this.setState((prevState,props)=>{
	a: prevState.a + 1 
    })
}

这两种方式有什么区别?

对象作为参数形式

对象做为setState的参数形式是开发中用的最多的,也是最常用的,只有在考虑到需要拿到state上次更新的值时,才会考虑使用函数作为setState的参数。最重要的一点就是一般情况下,对象作为参数的setState是异步的。这也是我们在开发过程中需要掌握setState的在某些其他情况会表现成同步。

函数作为参数形式

函数回调形式的两个参数prevStateprops。其中prevState是当前组件状态的引用。不能直接被修改。函数中接收的 prevState 和 props 都保证为最新。回调函数的返回值会与 prevState 进行浅合并。所以函数作为setState的参数时,在调用 setState 进行更新 state 时,React 会按照各个 setState 的调用顺序,将它们依次放入一个队列,然后,在进行状态更新时,则按照队列中的先后顺序依次调用,并将上一个调用结束时产生的 prevState传入到下一个调用的函数中,当然,第一个setState调用时,传入的 prevState 则是当前的 default state。

setState的特点

setState: React 中用于修改状态,更新视图。它具有以下特点: 异步与同步: setState并不是单纯的异步或同步,这其实与调用时的环境相关:

  • 在合成事件 和 生命周期钩子(除 componentDidUpdate) 中,setState是"异步"的;

    • 原因: 因为在setState的实现中,有一个判断: 当更新策略正在事务流的执行中时,该组件更新会被推入dirtyComponents队列中等待执行;否则,开始执行batchedUpdates队列更新;

      • 在生命周期钩子调用中,更新策略都处于更新之前,组件仍处于事务流中,而componentDidUpdate是在更新之后,此时组件已经不在事务流中了,因此则会同步执行;
      • 在合成事件中,React 是基于 事务流完成的事件委托机制 实现,也是处于事务流中;
    • 问题: 无法在setState后马上从this.state上获取更新后的值。

    • 解决: 如果需要马上同步去获取新值,setState其实是可以传入第二个参数的。setState(updater, callback),在回调中即可获取最新值;

  • 在 原生事件 和 setTimeout 中,setState是同步的,可以马上获取更新后的值;

    • 原因: 原生事件是浏览器本身的实现,与事务流无关,自然是同步;而setTimeout是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步;
  • 批量更新: 在 合成事件 和 生命周期钩子 中,setState更新队列时,存储的是 合并状态(Object.assign)。因此前面设置的 key 值会被后面所覆盖,最终只会执行一次更新;

  • 函数式: 由于 Fiber 及 合并 的问题,官方推荐可以传入 函数 的形式。setState(fn),在fn中返回新的state对象即可,例如this.setState((state, props) => newState);

    • 使用函数式,可以用于避免setState的批量更新的逻辑,传入的函数将会被 顺序调用;

注意事项:

  • setState 合并,在 合成事件 和 生命周期钩子 中多次连续调用会被优化为一次;

  • 当组件已被销毁,如果再次调用setState,React 会报错警告,通常有两种解决办法

    • 将数据挂载到外部,通过 props 传入,如放到 Redux 或 父级中;
    • 在组件内部维护一个状态量 (isUnmounted),componentWillUnmount中标记为 true,在setState前进行判断;

总结