写点什么

问:React 的 useState 和 setState 到底是同步还是异步呢?

作者:beifeng1996
  • 2022-10-28
    浙江
  • 本文字数:3471 字

    阅读完需:约 11 分钟

先来思考一个老生常谈的问题,setState是同步还是异步?


再深入思考一下,useState是同步还是异步呢?


我们来写几个 demo 试验一下。

先看 useState

同步和异步情况下,连续执行两个 useState 示例

function Component() {  const [a, setA] = useState(1)  const [b, setB] = useState('b')  console.log('render')
function handleClickWithPromise() { Promise.resolve().then(() => { setA((a) => a + 1) setB('bb') }) }
function handleClickWithoutPromise() { setA((a) => a + 1) setB('bb') }
return ( <Fragment> <button onClick={handleClickWithPromise}> {a}-{b} 异步执行 </button> <button onClick={handleClickWithoutPromise}> {a}-{b} 同步执行 </button> </Fragment> )}
复制代码


结论:


  • 当点击同步执行按钮时,只重新 render 了一次

  • 当点击异步执行按钮时,render 了两次

同步和异步情况下,连续执行两次同一个 useState 示例

function Component() {  const [a, setA] = useState(1)  console.log('a', a)
function handleClickWithPromise() { Promise.resolve().then(() => { setA((a) => a + 1) setA((a) => a + 1) }) }
function handleClickWithoutPromise() { setA((a) => a + 1) setA((a) => a + 1) }
return ( <Fragment> <button onClick={handleClickWithPromise}>{a} 异步执行</button> <button onClick={handleClickWithoutPromise}>{a} 同步执行</button> </Fragment> )}
复制代码


  • 当点击同步执行按钮时,两次 setA 都执行,但合并 render 了一次,打印 3

  • 当点击异步执行按钮时,两次 setA 各自 render 一次,分别打印 2,3

再看 setState

同步和异步情况下,连续执行两个 setState 示例

class Component extends React.Component {  constructor(props) {    super(props)    this.state = {      a: 1,      b: 'b',    }  }
handleClickWithPromise = () => { Promise.resolve().then(() => { this.setState({...this.state, a: 'aa'}) this.setState({...this.state, b: 'bb'}) }) }
handleClickWithoutPromise = () => { this.setState({...this.state, a: 'aa'}) this.setState({...this.state, b: 'bb'}) }
render() { console.log('render') return ( <Fragment> <button onClick={this.handleClickWithPromise}>异步执行</button> <button onClick={this.handleClickWithoutPromise}>同步执行</button> </Fragment> ) }}
复制代码


  • 当点击同步执行按钮时,只重新 render 了一次

  • 当点击异步执行按钮时,render 了两次


跟 useState 的结果一样

同步和异步情况下,连续执行两次同一个 setState 示例

class Component extends React.Component {  constructor(props) {    super(props)    this.state = {      a: 1,    }  }
handleClickWithPromise = () => { Promise.resolve().then(() => { this.setState({a: this.state.a + 1}) this.setState({a: this.state.a + 1}) }) }
handleClickWithoutPromise = () => { this.setState({a: this.state.a + 1}) this.setState({a: this.state.a + 1}) }
render() { console.log('a', this.state.a) return ( <Fragment> <button onClick={this.handleClickWithPromise}>异步执行</button> <button onClick={this.handleClickWithoutPromise}>同步执行</button> </Fragment> ) }}
复制代码


  • 当点击同步执行按钮时,两次 setState 合并,只执行了最后一次,打印 2

  • 当点击异步执行按钮时,两次 setState 各自 render 一次,分别打印 2,3


这里跟useState不同,同步执行时useState也会对state进行逐个处理,而setState则只会处理最后一次 参考:前端react面试题详细解答

为什么会有同步执行和异步执行结果不同呢?

这里就涉及到 react 的 batchUpdate 机制,合并更新。


  • 首先,为什么需要合并更新呢?


如果没有合并更新,在每次执行 useState 的时候,组件都要重新 render 一次,会造成无效渲染,浪费时间(因为最后一次渲染会覆盖掉前面所有的渲染效果)。所以 react 会把一些可以一起更新的 useState/setState 放在一起,进行合并更新。


  • 怎么进行合并更新


这里 react 用到了事务机制。


React 中的 Batch Update 是通过「Transaction」实现的。在 React 源码关于 Transaction 的部分,用一大段文字及一幅字符画解释了 Transaction 的作用:


*                       wrappers (injected at creation time)*                                      +        +*                                      |        |*                    +-----------------|--------|--------------+*                    |                 v        |              |*                    |      +---------------+   |              |*                    |   +--|    wrapper1   |---|----+         |*                    |   |  +---------------+   v    |         |*                    |   |          +-------------+  |         |*                    |   |     +----|   wrapper2  |--------+   |*                    |   |     |    +-------------+  |     |   |*                    |   |     |                     |     |   |*                    |   v     v                     v     v   | wrapper*                    | +---+ +---+   +---------+   +---+ +---+ | invariants* perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->*                    | |   | |   |   |         |   |   | |   | |*                    | |   | |   |   |         |   |   | |   | |*                    | |   | |   |   |         |   |   | |   | |*                    | +---+ +---+   +---------+   +---+ +---+ |*                    |  initialize                    close    |*                    +-----------------------------------------+
复制代码


用大白话说就是在实际的 useState/setState 前后各加了段逻辑给包了起来。只要是在同一个事务中的 setState 会进行合并(注意,useState 不会进行 state 的合并)处理。


  • 为什么 setTimeout 不能进行事务操作


由于 react 的事件委托机制,调用 onClick 执行的事件,是处于 react 的控制范围的。


而 setTimeout 已经超出了 react 的控制范围,react 无法对 setTimeout 的代码前后加上事务逻辑(除非 react 重写 setTimeout)。


所以当遇到 setTimeout/setInterval/Promise.then(fn)/fetch 回调/xhr 网络回调时,react 都是无法控制的。


相关 react 源码如下:


if (executionContext === NoContext) {  // Flush the synchronous work now, unless we're already working or inside  // a batch. This is intentionally inside scheduleUpdateOnFiber instead of  // scheduleCallbackForFiber to preserve the ability to schedule a callback  // without immediately flushing it. We only do this for user-initiated  // updates, to preserve historical behavior of legacy mode.  flushSyncCallbackQueue()}
复制代码


executionContext 代表了目前 react 所处的阶段,而 NoContext 你可以理解为是 react 已经没活干了的状态。而 flushSyncCallbackQueue 里面就会去同步调用我们的 this.setState ,也就是说会同步更新我们的 state 。所以,我们知道了,当 executionContext 为 NoContext 的时候,我们的 setState 就是同步的

总结

我们来总结一下上述实验的结果:


  1. 在正常的 react 的事件流里(如 onClick 等)


  • setState 和 useState 是异步执行的(不会立即更新 state 的结果)

  • 多次执行 setState 和 useState,只会调用一次重新渲染 render

  • 不同的是,setState 会进行 state 的合并,而 useState 则不会


  1. 在 setTimeout,Promise.then 等异步事件中


  • setState 和 useState 是同步执行的(立即更新 state 的结果)

  • 多次执行 setState 和 useState,每一次的执行 setState 和 useState,都会调用一次 render


是不是感觉有点绕,自己写一下代码体验一下就好了~


用户头像

beifeng1996

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
问:React的useState和setState到底是同步还是异步呢?_React_beifeng1996_InfoQ写作社区