setState是同步还是异步的?

·  阅读 189

通常认为setState是异步的,比如下面这个案例

class Test extends Component {
    state = {
        count: 0
    }

    componentDidMount(){
        this.setState({
           count: 1
         }, () => {
            console.log(this.state.count)
         })
        console.log(this.state.count) 
    }

    render(){
        ...
    }
}
复制代码

由于我们接受 setState 是异步的,所以会认为回调函数是异步回调,打出 0 的 console.log 会先执行,打出 1 的会后执行。

再看下面这个案例

class Test extends Component {
    state = {
        count: 0
    }

    componentDidMount(){
        this.setState({
           count: this.state.count + 1
         }, () => {
            console.log(this.state.count)
         })
         this.setState({
           count: this.state.count + 1
         }, () => {
            console.log(this.state.count)
         })
    }

    render(){
        ...
    }
}
复制代码

你可能认为打印出来的1,2,如果重新仔细思考,你会发现当前拿到的 this.state.count 的值并没有变化,都是 0,所以输出结果应该是 1,1。

那接下来这个案例的答案是什么呢?

class Test extends Component {
    state = {
        count: 0
    }

    componentDidMount(){
        this.setState(
          preState=> ({
            count:preState.count + 1
        }),()=>{
           console.log(this.state.count)
        })
        this.setState(
          preState=>({
            count:preState.count + 1
        }),()=>{
           console.log(this.state.count)
        })
    }

    render(){
        ...
    }
}

复制代码

这些通通是异步的回调,如果你以为输出结果是 1,2,那就又错了,实际上是 2,2。

为什么会这样呢?当调用 setState 函数时,就会把当前的操作放入队列中。React 根据队列内容,合并 state 数据,完成后再逐一执行回调,根据结果更新虚拟 DOM,触发渲染。所以回调时,state 已经合并计算完成了,输出的结果就是 2,2 了。

这非常反直觉,那为什么 React 团队选择了这样一个行为模式,而不是同步进行呢?一种常见的说法是为了优化。通过异步的操作方式,累积更新后,批量合并处理,减少渲染次数,提升性能。但同步就不能批量合并吗?这显然不能完全作为 setState 设计成异步的理由。

在 17 年的时候就有人提出这样一个疑问“为什么 setState 是异步的”,这个问题得到了官方团队的回复,原因有 2 个。

  • 保持内部一致性。如果改为同步更新的方式,尽管 setState 变成了同步,但是 props 不是。
  • 为后续的架构升级启用并发更新。为了完成异步渲染,React 会在 setState 时,根据它们的数据来源分配不同的优先级,这些数据来源有:事件回调句柄、动画效果等,再根据优先级并发处理,提升渲染性能。

从 React 17 的角度分析,异步的设计无疑是正确的,使异步渲染等最终能在 React 落地。那什么情况下它是同步的呢?

同步场景

class Test extends Component {
    state = {
        count: 0
    }

    componentDidMount(){
        this.setState({ count: this.state.count + 1 });
        console.log(this.state.count);
        setTimeout(() => {
          this.setState({ count: this.state.count + 1 });
          console.log("setTimeout: " + this.state.count);
        }, 0);
    }

    render(){
        ...
    }
}

复制代码

那这时输出的应该是什么呢?如果你认为是 0,0,那么又错了。

正确的结果是 0,2。因为 setState 并不是真正的异步函数,它实际上是通过队列延迟执行操作实现的,通过 isBatchingUpdates 来判断 setState 是先存进 state 队列还是直接更新。值为 true 则执行异步操作,false 则直接同步更新。

在 onClick、onFocus 等事件中,由于合成事件封装了一层,所以可以将 isBatchingUpdates 的状态更新为 true;在 React 的生命周期函数中,同样可以将 isBatchingUpdates 的状态更新为 true。那么在 React 自己的生命周期事件和合成事件中,可以拿到 isBatchingUpdates 的控制权,将状态放进队列,控制执行节奏。而在外部的原生事件中,并没有外层的封装与拦截,无法更新 isBatchingUpdates 的状态为 true。这就造成 isBatchingUpdates 的状态只会为 false,且立即执行。所以在 addEventListener 、setTimeout、setInterval 这些原生事件中都会同步更新。

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改