本文已参与「新人创作礼」活动,一起开启掘金创作之路。 详情

setState异步更新

我们都知道, React 通过 this.state 来访问 state ,通过 this.setState() 方法来更新 state 。当 this.setState() 方法被调用的时候, React 会重新调用 render 方法来重新渲染 UI

那么setState任何时候都是异步的吗?

首先如果直接在 setState 后面获取 state 的值是获取不到的。在 React 内部机制能检测到的地方, setState 就是异步的;在React检测不到的地方,例如 原生事件 addEventListener , setInterval , setTimeout setState 就是同步更新的

✔setState是同步还是异步呢?

setState并不是单纯的异步或同步,这其实与调用时的环境相关

  • 合成事件 生命周期钩子 (除 componentDidUpdate ) 中, setState 是"异步"的;
  • 原生事件 setTimeout 中, setState 是同步的,可以马上获取更新后的值;
  • 批量更新 :多个顺序的 setState 不是同步地一个一个执行滴,会一个一个加入队列,然后最后一起执行。在 合成事件 和 生命周期钩子 中,setState更新队列时,存储的是 合并状态(Object.assign)。因此前面设置的 key 值会被后面所覆盖,最终只会执行一次更新。
  • 函数式: setState第一个参数为 函数形式 时,在这个函数中可以回调拿到最新的state对象,然后函数return出的对象讲被设置成newState。 this.setState((state, props) => newState)
  • 所谓异步?

    setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的 callback 拿到更新后的结果

    批量更新:

    setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和 setTimeout 中不会批量更新。① 在“异步”中如果对同一个值进行多次 setState setState 的批量更新策略会对其进行覆盖, 取最后一次的执行 ,② 如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

    为什么要setState"异步化"批量处理呢?

  • 做成异步设计是为了性能优化,减少渲染次数
  • 保持内部一致性。如果将 state 改为同步更新,那尽管 state 的更新是同步的,但是 props不是
  • 启用并发更新,完成异步渲染。
  • setState原理

    setState 并非真异步,只是看上去像异步。在源码中,通过 isBatchingUpdates 来判断

    setState调用流程:

    ①调用 this.setState(newState) -> ②将新状态newState存入pending队列 -> ③判断是否处于 batch Update isBatchingUpdates 是否为true) -> ④ isBatchingUpdates =true,保存组件于 dirtyComponents 中,走异步更新流程,合并操作,延迟更新;

    isBatchingUpdates =false,走同步过程。遍历所有的 dirtyComponents ,调用 updateComponent ,更新pending state or props

    setState批量更新的过程

    react 生命周期和合成事件执行前后都有相应的钩子,分别是 pre 钩子和 post 钩子

    pre 钩子会调用 batchedUpdate 方法将 isBatchingUpdates 变量置为 true ,也就是将状态标记为现在正处于更新阶段了。开启批量更新。

    setState 的更新会被存入队列中,待同步代码执行完后,再执行队列中的 state 更新。 isBatchingUpdates 若为 true ,则把当前组件(即调用了 setState 的组件)放入 dirtyComponents 数组中;否则 batchUpdate 所有队列中的更新

    为什么直接修改this.state无效

  • setState 本质是通过一个队列机制实现 state 更新的。 执行 setState 时,会将需要更新的state合并后放入状态队列,而不会立刻更新 state ,队列机制可以批量更新 state
  • 如果不通过 setState 而直接修改 this.state ,那么这个 state 不会放入状态队列中,下次调用 setState 时对状态队列进行合并时,会忽略之前直接被修改的 state ,这样我们就无法合并了,而且实际也没有把你想要的 state 更新上去
  • setState之后发生的事情

    setState 调用后,React会去 diff state ,若state变化然后会去 diff DOM 判断是否更新UI。如果每次setState都去走这些流程可能就会有性能问题。

    所有短时间内多次的 setState 时,React会将state的改变压入栈中,在合适的时机,批量更新 state 和视图,达到提高性能的效果。

    setState循环调用风险

  • 当调用 setState 时,实际上会执行 enqueueSetState 方法,并对 partialState 以及 _pending-StateQueue 更新队列进行合并操作,最终通过 enqueueUpdate 执行 state 更新
  • performUpdateIfNecessary 方法会获 取_pendingElement , _pendingStateQueue _pending-ForceUpdate ,并调用 receiveComponent updateComponent 方法进行组件更新
  • 如果在 shouldComponentUpdate 或者 componentWillUpdate 方法中调用 setState ,此时 this._pending-StateQueue != null ,就会造成循环调用,使得浏览器内存占满后崩溃
  • 判断state输出

    看第一个例子: setState的同步异步:

    class Test extends React.Component {
      state  = {
          val: 0
      componentDidMount() {
        this.setState({ val: this.state.val + 1 });
        console.log(this.state.val); // 0
        setTimeout(() => {
          this.setState({ val: this.state.val + 1 });
          console.log("setTimeout: " + this.state.val);  // 2
        }, 0);
      render() {
        return null;
    

    输出结果0 2。过程解析:

    ① 直接在componentDidMount生命周期中的setState是异步的,此时的val+1并不会立即生效。所以下面第一个的log输出不会拿到最新的值,还是拿到的之前的值,输出0

    ② 在setTimeout中的setState是同步的,此时的val可以拿到最新的值,也就是①中最新的val值为1,此时再调用setState同步给val+1,同步得到val值为2,所以第二次的log输出就是2

    再看一个例子:异步中setState的批量更新:

      componentDidMount() {
        this.setState({val: this.state.val + 1});
        console.log(this.state.val);    // 输出0
        this.setState({val: this.state.val + 1});
        console.log(this.state.val);    // 输出0
        setTimeout(() => {
          this.setState({val: this.state.val + 1});
          console.log(this.state.val);  // 输出2
          this.setState({val: this.state.val + 1});
          console.log(this.state.val);  // 输出3
        }, 0);
    

    输出结果:0 0 2 3。过程解析:

    ①前两次的输出都是直接在生命周期中的输出,所以是异步过程,拿不到最新的值,结果输出都是0;

    ②前两次的setState都是设置的val这同一个key值,更新操作会被覆盖,只执行最后一次,所以相当于只执行了一次val+1

    ③在setTimeout中时已经取到了最新的val值为1,此时调用setState为同步了,执行完setTimeout中的第一个val+1后val=2,这时候的第三个log输出即为2。

    setTimeout中的第二次setState同理,为同步过程,直接在val+1=3后的log输出就直接是最新值3

    再看关于setState的第二个参数取值

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

    输出结果是1 1,你猜对了吗?看一下过程解析:

    ① 在生命周期中的setState是异步的,此时设置同一个state的key值,操作会被覆盖,相当于只执行了一次count+1,所以实际上的count的值为1

    ② 我们知道setState的第二个参数是在更新完成后的回调,可以拿到最新的state。

    ③ 但是setState时这些回调都是会先把操作函数注入到队列中,等state的批量更新完成后再挨个执行这些回调。实际上执行这两个回调的时机是批量更新后依次执行的,此时的count是1,所以两个都是输出1。

    ④ 所以可以理解为两次的setState是合并在一起覆盖后只剩下一个。它们各自的回调是合并在一起执行的,所以都输出1

    如果我们使用preState

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

    输出结果为2 2。我们知道setState的第一个参数可以直接是一个对象表示newState,也可以是一个回调函数,拿到上一次的state然后经过操作再return一个newState对象。所以这个执行流程就是:

    ① 第一个setState时,拿到上一次的count=0后执行+1操作,count变成了1

    ②第二个setState时,回调参数的preState中的count就是1,此时的+1操作就变成了2

    ③ 两次的setState的第二个回调参数同时依次执行,输出结果都是最新的count为2

    其原因还是由于批量更新。① 如果setState第一个参数是对象,就存储其合并状态(Object.assign)。因此前面设置的 key 值会被后面所覆盖,多次修改同个key值的结果是最终只会执行一次更新;②当第一个参数是函数时,上面两个setState的操作会存两个函数在队列中。会执行了第一个函数后改变合并状态(Object.assign)中的这个key的值(count为1),然后再执行第二个函数时从最新状态获取后再count+1即为2,return出的对象再set合并状态(Object.assign)中的key值count的值为2了。

    异步过程的总结

  • 通过setState去更新this.state,不要直接操作this.state,请把它当成不可变的
  • 调用setState更新this.state不是马上生效的,它是异步的,所以不要天真以为执行完setStatethis.state就是最新的值了
  • 多个顺序执行的setState不是同步地一个一个执行滴,会一个一个加入队列,然后最后一起执行,即批处理
  • Fairy_妍 25.2k
    粉丝