加加 React 30 问 -- 1. setState 是同步还是异步

634 阅读3分钟

1. setState 是同步还是异步

要想回答这个问题,首先先看例子,

import React, { Component } from 'react'
import './App.css';

class ClsApp extends Component {
    state = {
        num: 0
    }
    updateNum = () => {
        console.log('setState 前',this.state.num)
        this.setState({ num: this.state.num + 1 })
        console.log('setState 后 -- 正常',this.state)

        // setTimeout(() => {
        //     this.setState({ num: this.state.num + 1 })
        //     console.log('setState 后 -- setTimeout',this.state)
        // })
    }

    render() {
        return (
            <>
                <button onClick={this.updateNum}>+1</button>
                <p>{this.state.num}</p>
            </>
        )
    }
}

export default ClsApp

在这个例子中,正常点击触发更新,this.state 的值前后一致,我们说它是异步的,而如果放在 setTimeout 中,前后不一致,我们说它是同步的,那么到底是怎么肥事呢?

考虑 setState 是同步还是异步是没有意义的

  • 回到上面这个例子,无论是同步还是异步,我们都能在视图中得到我们想要的更新过后的结果,而当我们需要做一些依赖更新过后的值的操作,本身就不应该编写在 setState 之后。
  • 如果我们想要依赖更新之后的状态值
    • 在 ClassComponent 中,我们可以在 componentDidMounted 或者 componentDidUpdate 中执行
    • 在 FunctionComponent 中,我们可以在 useEffect 的回调函数中执行
  • 在业务中一旦需要考虑这个特性,大概率都是写法出问题了,推荐按照正规的写法重新编码,而不是在纠结它的同步异步特性。

React 在不同 mode 下,对 setState 触发的更新有不同的处理

legacy模式 -- ReactDOM.render

  • 默认情况下使用 useSate 的是异步的,原因是 React 内部有一个性能优化机制 -- batchedUpdates 批处理
    • 这个性能优化机制是为了多次调用 setState 触发更新的时候,合并成一个更新,减少因状态更新引擎的页面渲染过多而导致的性能问题。
    • 在 react 的声明周期函数中调用 setState,会执行 batchedUpdates,这个时候会首先加入一个 batchedContext 的标志,有这个标志就会使得 setState 的 action 放入到 batchedUpdateQueue 中
    • 因此,调用 setState 之后,不会立刻执行更新,而是要将(1-n个) useState 的 action 放在 batchedUpdateQueue 队列中的。等到 react 上下文结束了,又会取消 batched 标志,这个时候就同步执行响应的回调了。
  • 当我们用 setTimeout 包裹 setState 的时候,当执行的时候,不会走 react 内部的 batchedUpdates 优化机制, 此时运行上下文为 noContext;在调度更新过程中,如果是同步优先级 + noContext 上下文,就会执行同步执行回调队列 flushSyncCallbackQueue.
  • 因此在 legacy 模式下,如果是在可以触发 react 机制的位置上使用 setState, 是异步执行的,如果是 setTimeout 这种不会触发 react 机制的上下包裹下使用 setState,是同步的。

Concurrent 模式下 -- 暂时还属于试验阶段, ReactDOM.unstable_createRoot().render()

  • 在 legacy 中,使用 setTimeout 包裹 setState 会是同步执行的,原因是 legacy 模式下,FiberRoot 对应的 lane 是 Synclane 同步优先级
    • 在同步优先级的时候,如果上下文是 noContext,才会同步执行
  • 而在 Concurrent 模式下,是异步优先级,所以所有的更新都是异步的
  • 因此无论怎么处理,使用 setState 触发的更新都是异步的。

小结

  • Legacy 模式下,命中 batchUpdates 时是异步的,未命中时是同步的
  • 在 Concurrent 模式下都是异步的

参考