setState 是同步更新还是异步更新?

1,622 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 22 天,点击查看活动详情

大家好,我是爱吃鱼的桶哥Z。在React16.8之前的版本中,我们更新数据需要用到setState,那么你知道setState是同步还是异步的呢?它内部是如何实现的,你了解吗?今天我们就一起来学习一下关于setState的那些事吧!

setState

自从React16.8添加了Hook后,我们编写React组件基本都是函数组件,很少用到class组件了。我们都知道在函数组件中通过useState这个Hook来修改组件的状态,而在16.8之前的版本中,我们都是通过setState来修改组件的状态,因此我们还是很有必要了解一下关于setState相关的知识点。

在面试或者工作中,我们经常会遇到关于组件状态更新的问题,就拿setState来说,在组件更新的时候,setState是同步更新还是异步更新的?当我们更新一个组件的状态后,我们如何才能立即获取到刚才更新的状态?这些就是我们需要学习和了解的地方。要了解setState是同步还是异步,就先需要了解一下React中的合成事件了。

如果了解过React事件相关方面的童鞋,那么就会知道React的事件都是合成事件,而不是js的原生事件。那什么是合成事件呢?简单来说就是React通过给document上挂载一个事件监听函数,通过事件冒泡的方式来完成事件的执行,当DOM元素触发后会冒泡到document,而React就会找到对应的组件生成一个合成事件出来,并按组件树模拟一遍事件冒泡,这就是React中的合成事件

当然,上述的方法在React17之后的版本中进行了修改。React17中将事件挂载在了DOM的容器中,也就是挂载在React DOM执行的节点上,这样修改的好处是哪怕一个项目中有多个版本的React存在,组件的事件也不会乱套。那么哪些事件会被捕获生成合成事件呢?在React源码的事件快照中所包含的事件才会被捕获到,例如:clickblurfocus等等。

了解完合成事件,我们该继续学习setState是同步还是异步的。一般来说setState是异步的,例如下面这个例子:

clsss Test extends Component {
    state = {
        count: 0
    };
    
    componentDidMount() {
        this.setState({
            count: 1
        }, () => {
            console.log(this.state.count); // 1
        });
        
        console.log(this.state.count); // 0
    }
    
    render() {
        return (
            ...
        )
    }
}

当我们在componentDidMount生命周期中通过setState修改组件的状态后,我们只有在setState的第二个参数中才能立即获取到当前修改的值,而在外部获取到的值还是未改变之前的值,由此可以证明setState是异步的。我们是否会觉得React中的setState执行像是一个队列,因为React会根据队列逐一执行,并且合并setState的操作,当state数据完成后执行回调,然后根据结果来更新虚拟DOM触发渲染。那么为什么React官方团队要按这样的执行思路来实现呢?为什么不能用同步执行的思路来实现呢?

React17出来后,官方给出的解释的是:为了保持内部的一致性,如果setState是同步的,但是props却不是,就会导致数据的错乱;第二点就是为了后续的升级启用并发更新。那么什么情况下setState是同步的呢?

如果我们将setState放在setTimeout或者setInterval中,那么它们的执行就会跟上面将的完全不同了,大致的代码如下:

clsss Test extends Component {
    state = {
        count: 0
    };
    
    componentDidMount() {
        this.setState({
            count: this.state.count + 1
        });
        console.log(this.state.count); // 0
        setTimeout(() => {
            this.setState({
                count: this.state.count + 1
            });
            console.log('setTimeout', this.state.count); // 1
        }, 0);
    }
    
    render() {
        return (
            ...
        )
    }
}

setState执行时,会将状态存入padding队列中,然后React会判断当前是否处于batch update阶段,如果是,则会将组件存入dirtyComponents中;反之则会遍历所有的dirtyComponents,并调用updateComponent用于更新pendingstate或者props

React的生命周期事件和合成事件中可以获取到isBatchingUpdates的控制权,用于将状态放入队列中,并控制执行的节奏。而在setTimeoutaddEventListener这些原生事件中,无法获取到isBatchingUpdates的控制权,就会导致isBatchingUpdates只会为false,且会一直执行,因此setState就会同步执行并修改组件的状态。

最后

setState其实并不是真的异步,只是看起来像是异步执行的,它是通过isBatchingUpdates来判断当前执行是同步还是异步的,如果isBatchingUpdatestrue,则按异步执行,反之就是同步执行。要改变isBatchingUpdates,只需要打破React的合成事件,在js的原生事件中执行setState即可,所以你知道setState是同步还是异步的吗?

最后,如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家

往期回顾

在 React 中为什么要用JSX?