setState可能是异步的

330 阅读3分钟

提到React相信setState的这个函数你应该不会陌生,这是我们唯一可以修改state并让触发组件变化的方法。但是你真的会用吗?

一个简单的例子

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;

如果你知道我这个例子在说什么,那么可以点击右上角关闭这篇文章了,否则请看下图演示:

Bad Counter

可以看到当我们点击了increment按钮后,数值只增加了1而不是2。这就是我们今天要讨论的问题了。

如何解决这个问题?

实际上我们经常使用的setState方法其实还有第二个参数,一个可选的回调函数,具体定义如下:

setState(updater[, callback])

一旦setState完成并且组件重绘之后,这个回调函数将会被调用。所以当我们使用回调函数这种异步的调用方式的时候就可以避免之前的错误。如果不好理解的话可以想象setState是一个网络请求,使用回调函数才能在正确的时间拿到正确的值。

我们只需要简单的修改incrementCount方法如下:

incrementCount = () => {
    // this.setState({count : this.state.count + 1}) 
    // this.setState({count : this.state.count + 1})
    this.setState(prevState => {
        return {count: prevState.count + 1}
    });
    this.setState(prevState => {
        return {count: prevState.count + 1}
    });
}

为什么会这样?

其实当你再去查阅React官网这篇文章时候,你不难发现有提到下面一段话:

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall

从这句话中我们看到setState()方法有可能不是立刻更新组件的,他可能会合并一些更新或者推迟一些更新。我们下面就来简单解释一下(因为涉及到一些源码的实现,对于初学者不太好理解这里就不讲的太复杂,大概看一下就好了

因为在setState的函数实现中,会有一个isBatchingUpdates(默认是false)来判断是直接更新还是回头再说。React在调用事务处理函数(生命周期函数)之前会调用batchedUpdates函数把isBatchingUpdates设置为true,这样在事件处理过程中就不会同步更新了,而是合并一些setState操作。在我们一开始的例子中两次加1的操作其实是被React做了一个浅拷贝合并在一起生成新的值,其实相当于

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

这样后来的这次加1就把前面的这次给覆盖了,所以你就只能看到加1了。

什么时候需要使用回调的方式?

当你本次需要setState设置的值是依赖于props或者state计算得到的时候就需要使用回调的方式。

this.setState((prevState, props) => {
  return {
      //这里设置属性
  };
});