React 学习之深入认识 setState

600 阅读3分钟

在 React Hook 出现之前,我们使用类组件来存储当前组件的状态,但是我们在更新数据时,有时候会出现一些与预期结果有差异的问题

举个栗子🌰

组件状态更新、页面渲染时机与预期结果可能存在一些出入

import React, { Component } from 'react'
class StateComp extends Component {
    state = {
        num: 0
    }
    handlePlus = () => {
        this.setState({
            num: this.state.num + 1
        })
        console.log(this.state.num) // 这里还是上一个 num 值
    }
    render() {
        console.log('render')
        return (
            <div>
                <p>{this.state.num}</p>
                <p>
                    <button onClick={this.handlePlus}>Plus</button>
                </p>
            </div>
        )
    }
}

export default StateComp

上面的🌰中,我们可以在控制台看到第一次点击 Plus 按钮之后的输出内容是这样的:

0
'render'

输出顺序自然没有问题 (即更新状态,再触发页面渲染),但既然是在 render 之前,那么我们有理由认为打印的数据 num 应该是更新后的值 1 才对啊...

这是因为 setState 对状态的改变可能是异步的,而 render 方法是在数据改变后触发的,并不是调用 setState 方法后就立即触发;若是立即触发的话,那上面的输出顺序就需要颠倒,结果就应该是:

'render'
1

React 出于性能考虑,会把 异步 的多个 setState() 调用合并成一个调用。同步 调用 setState 则不会进行合并 更新 (如后面的第二个和第三个🌰所示)

如果改变状态的代码处于某个 HTML 元素的事件中,则它是异步的,否则是同步的。所以,如果在某个事件中,需要连续调用多次,则需要使用函数的方式获取最新的状态。

使用示例如下:

// 连续加两次,但更新只有一次
// 返回值会混合覆盖掉之前的状态 (num 属性值)

// onClick 事件处理函数中
handlePlus = () => {
    this.setState(prevState => ({
        num: prevState.num + 1
    }), () => {
        // 在 render 之后执行,num 为两次 +1 后的结果
        console.log('state 更新完成!', this.state.num)
    })
    this.setState(prevState => ({
        num: prevState.num + 1
    }))
}

类似这种 DOM 事件异步操作的情况,this.setState 需要使用回调函数来获取更新后的状态值:

  1. 第一个参数使用回调函数接受两个参数,即:上次操作后更新后的 state当前的 props

  2. 第二个参数则可以拿到多个 异步 setState 操作后的状态值,执行时机为:所有状态更新完成,并且完成页面渲染之后

再看一个🌰

import React, { Component } from 'react'
class StateComp extends Component {
    state = {
        num: 0
    }
    handlePlus = () => {
        const timer = setTimeout(() => {
          this.setState({
            n: this.state.num + 1
          });
          this.setState({
            n: this.state.num + 1
          });
          this.setState({
            n: this.state.num + 1
          });
          console.log('end num: ', this.state.num); // 3
          // clearTimeout(timer);
        }, 1000);
        console.log('start num: ', this.state.num); // 0
    }
    render() {
        console.log('render')
        return (
            <div>
                <p>{this.state.num}</p>
                <p>
                    <button onClick={this.handlePlus}>Plus</button>
                </p>
            </div>
        )
    }
}

export default StateComp

如上示例代码所示,按钮点击触发 1s 后三次连续调用 setState,因是同步设置,会三次触发 render,打印内容为:

'start num:' 0
'render'
'render'
'render'
'end num:' 3

还是上面的🌰(稍稍改动)

import React, { Component } from 'react'
class StateComp extends Component {
    state = {
        num: 0
    }
    constructor(props) {
        super(props)
        // setInterval 内部调用 setState 是同步调用的方式
        this.timer = setInterval(() => {
            this.setState({
                num: this.state.num + 1
            })
            this.setState({
                num: this.state.num + 1
            })
            this.setState({
                num: this.state.num + 1
            })
            clearInterval(this.timer)
        }, 1000)
    }
    render() {
        console.log('render')
        return (
            <div>
                {this.state.num}
            </div>
        )
    }
}

export default StateComp

构造函数中起一个定时器,每 1s 后三次调用 setState 更改 state,则 每隔 1s 都会打印三次 'render'。而且直接使用 this.state.num 的值也是正确的 (即均是更新后的值)

最佳实践

综上所述,我们可以总结一下类组件更新状态的最佳使用方案:

  1. 编码过程中将所有的 setState 都当作异步操作的处理方式 (虽然本人更倾向于使用 hook)
  2. 不要信任 setState 调用后的状态
  3. 若要使用改变后的状态,则应该使用回调函数的方式,(setState 第二个参数的回调函数会在页面渲染之后执行,可以取到正确的 state)
  4. 若新的状态需要根据之前的状态进行计算,则使用函数的方式改变状态,即 setState 第一个参数使用回调函数的方式