React的setState到底是异步还是同步

  1. 结论
  2. 代码解析
  3. 批量更新
  4. 同步背后的现象浅析

结论

ReactsetState 到底是异步还是同步?先给出结论:

  • setState 在 React 事件处理函数中或 React 生命周期方法中是异步
  • 在 setTimeout , Promise 等异步方法中或原生事件中是同步
    注意以上是在React 18以前的版本处理方式,18 开始使用 createRoot 的 api 的话,就不会有这种问题了,就算是 setTimeout 里的代码也能异步批量执行(同样,使用HooksuseState也不会有这种问题)。下文也是基于 React 18 前的版本class组件进行分析。

代码解析

如下代码片段:

class App extends React.Component {
    constructor(){
        super()
        this.state = {
            count: 0
        }
    }
  increment = () => {
    console.log('increment setState前', this.state.count)
    this.setState({
      count: this.state.count + 1
    })
    console.log('increment setState后', this.state.count)
  }

  triple = () => {
    console.log('triple setState前', this.state.count)
    this.setState({
      count: this.state.count + 1
    })
    this.setState({
      count: this.state.count + 1
    })
    this.setState({
      count: this.state.count + 1
    })
    console.log('triple setState后', this.state.count)
  }

  reduce = () => {
    setTimeout(()=>{
      console.log('reduce setState前', this.state.count)
      this.setState({
        count: this.state.count - 1
      })
      console.log('reduce setState后', this.state.count)
    }, 0)
  }

  render(){
    return(
      <div>
        <button onClick={this.increment}>点击增加</button>
        <button onClick={this.triple}>点击三倍增加</button>
        <button onClick={this.reduce}>点击减少</button>
      </div>
    )
  }
}

依次点击后,控制台打印结果:

increment setState前 0
increment setState后 0
triple setState前 1
triple setState后 1
reduce setState前 2
reduce setState后 1 // React 18使用 createRoot 此处会打印: reduce setState后 2

从打印信息及其顺序可能会有疑惑,incrementtriple两个函数 setState 是个异步操作,点击后state并不会立即发生改变,但reduce是怎么回事,setState又成了同步更新了?

批量更新

带着这个疑问,我们来看一下setState的执行流程:
setState执行流程
从上图可以看出,每次setState都会执行一次这个流程,消耗就会很大,类似如下流程:

this.setState({
  count:thisstate.count+1 // ===> shouldComponentUpdate ===> componentWillUpdate ===> render ===> componentDidUpdate
})

this.setState({
  count:this.state.count+1 // ===> shouldComponentUpdate ===> componentWillUpdate ===> render ===> componentDidUpdate
})

this.setState({
  count:this.state.count+1 // ===> shouldComponentUpdate ===> componentWillUpdate ===> render ===> componentDidUpdate
})

其实不然,ReactsetState 采用的是批量更新的机制,每进行一次setState就会把它塞进一个队列,等“时机成熟”,再把队列内积攒的state结果做合并,最后只针对最新的state值走一次更新流程,类似VuenextTick和浏览器的EventLoop。如下:

this.setState({
  count:thisStatecount+1 // ===> 入队,[count+l的任务]
})

this.setState({
  count:this.state.count+1 // ===> 入队,[cont+1的任务,count+l的任务]
})
this.setState({
  count:thisstate.count+1 // ===> [count+1的任务,count+1的任务,count+l的任务,合并state,[count+1的任务]
})
                         // ⇊
                         // 合并state,[count+1的任务]
                         // ⇊
                         // 执行count+1的任务

所以,只要同步队列没结束,就会一直这么“攒”下去。所以即使像下面这样setState100次,也只是入队列的次数在增加,并不会执行100次render,当100次入队列任务结束后,只是队列的内容发生变化,state本身并不会立刻改变。

test=()=>{
  console.log('循环100次setState前的count', this.state.count)
  for(leti=0; i<100; i++){
    this.setState({
      count: this.state.count+1
    })
  }
  console.log('循环100次setState后的count', this.state.count)
}
// 循环100次setState前的count 0
// 循环100次setState前的count 0

同步背后的现象浅析

并不是 setTimeout 改变了 setState,而是setTimeout帮助setState“逃脱”了React对它的管控只要是在React管控下的setState,一定是异步的。

ReactsetState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新this.state 还是放到一个 updateQueue 中延时更新。
isBatchingUpdates 默认是 false,表示 setState 会同步更新 this.state
但是,有一个函数 batchedUpdates,该函数会把 isBatchingUpdates 修改为 true。而当 React 在调用事件处理函数之前就会先调用这个 batchedUpdatesisBatchingUpdates修改为true。这样由 React 控制的事件处理过程 setState 不会同步更新 this.state,而是异步的。
所以说setState本身是同步的,一旦走了react内部的合并逻辑,放入了updateQueue队列中就变成异步了。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 chaoyumail@126.com

×

喜欢就点赞,疼爱就打赏