深入剖析setState同步异步机制

关于 setState

setState 的更新是同步还是异步,一直是人们津津乐道的话题。不过,实际上如果我们需要用到更新后的状态值,并不需要强依赖其同步/异步更新机制。在类组件中,我们可以通过this.setState的第二参数、componentDidMount、componentDidUpdate等手段来取得更新后的值;而在函数式组件中,则可以通过useEffect来获取更新后的状态。所以这个问题,其实有点无聊。

不过,既然大家都这么乐于讨论,今天我们就系统地梳理一下这个问题,主要分为两方面来说:

类组件(class-component)的更新机制

函数式组件(function-component)的更新机制

类组件中的 this.setState

在类组件中,这个问题的答案是多样的,首先抛第一个结论:

在legacy模式中,更新可能为同步,也可能为异步;

在concurrent模式中,一定是异步。

问题一、legacy 模式和 concurrent 模式是什么鬼?

通过ReactDOM.render(<App />, rootNode)方式创建应用,则为 legacy 模式,这也是create-react-app目前采用的默认模式;

通过ReactDOM.unstable_createRoot(rootNode).render(<App />)方式创建的应用,则为concurrent模式,这个模式目前只是一个实验阶段的产物,还不成熟。

legacy 模式下可能同步,也可能异步?

是的,这不是玄学,我们来先抛出结论,再来逐步解释它。

当直接调用时this.setState时,为异步更新;

当在异步函数的回调中调用this.setState,则为同步更新;

当放在自定义 DOM 事件的处理函数中时,也是同步更新。

实验代码如下:

class StateDemo extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            count0
        }
    }
    render() {
        return <div>
            <p>{this.state.count}</p>
            <button onClick={this.increase}>累加</button>
        </div>

    }
    increase = () => {
        this.setState({
            countthis.state.count + 1
        })
        // 异步的,拿不到最新值
        console.log('count'this.state.count)

        // setTimeout 中 setState 是同步的
        setTimeout(() => {
            this.setState({
                countthis.state.count + 1
            })
            // 同步的,可以拿到
            console.log('count in setTimeout'this.state.count)
        }, 0)
    }

    bodyClickHandler = () => {
        this.setState({
            countthis.state.count + 1
        })
        // 可以取到最新值
        console.log('count in body event'this.state.count)
    }

    componentDidMount() {
        // 自己定义的 DOM 事件,setState 是同步的
        document.body.addEventListener('click'this.bodyClickHandler)
    }
    componentWillUnmount() {
        // 及时销毁自定义 DOM 事件
        document.body.removeEventListener('click'this.bodyClickHandler)
    }
}

要解答上述现象,就必须了解 setState 的主流程,以及 react 中的 batchUpdate 机制。

首先我们来看看 setState 的主流程:

调用this.setState(newState);

newState会存入 pending 队列; 3,判断是不是batchUpdate; 4,如果是batchUpdate,则将组件先保存在所谓的脏组件dirtyComponents中;如果不是batchUpdate,那么就遍历所有的脏组件,并更新它们。

由此我们可以判定:所谓的异步更新,都命中了batchUpdate,先保存在脏组件中就完事;而同步更新,总是会去更新所有的脏组件。

非常有意思,看来是否命中batchUpdate是关键。问题也随之而来了,为啥直接调用就能命中batchUpdate,而放在异步回调里或者自定义 DOM 事件中就命中不了呢?

这就涉及到一个很有意思的知识点:react 中函数的调用模式。对于刚刚的 increase 函数,还有一些我们看不到的东西,现在我们通过魔法让其显现出来:

increase = () => {
        // 开始:默认处于bashUpdate
        // isBatchingUpdates = true
        this.setState({
            countthis.state.count + 1
        })
        console.log('count'this.state.count)
        // 结束
        // isBatchingUpdates = false

    }
    increase = () => {
        // 开始:默认处于bashUpdate
        // isBatchingUpdates = true
        setTimeout(() => {
            // 此时isBatchingUpdates已经设置为了false
            this.setState({
                countthis.state.count + 1
            })
            console.log('count in setTimeout'this.state.count)
        }, 0)
        // 结束
        // isBatchingUpdates = false
    }

当 react 执行我们所书写的函数时,会默认在首位设置isBatchingUpdates变量。看到其中的差异了吗?当 setTimeout 执行其回调时,isBatchingUpdates早已经在同步代码的末尾被置为false了,所以没命中batchUpdate。

那自定义 DOM 事件又是怎么回事?代码依然如下:

  componentDidMount() {
    // 开始:默认处于bashUpdate
    // isBatchingUpdates = true
    document.body.addEventListener("click", () => {
      // 在回调函数里面,当点击事件触发的时候,isBatchingUpdates早就已经设为false了
      this.setState({
        countthis.state.count + 1,
      });
      console.log("count in body event"this.state.count); // 可以取到最新值。
    });
    // 结束
    // isBatchingUpdates = false
  }

我们可以看到,当componentDidMount跑完时,isBatchingUpdates已经设置为false了,而点击事件后来触发,并调用回调函数时,取得的isBatchingUpdates当然也是false,不会命中batchUpdate机制。

总结:

this.setState是同步还是异步,关键就是看能否命中batchUpdate机制

能不能命中,就是看isBatchingUpdates是true还是false

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpzzyp.html