乐趣区

关于react.js:问React的useState和setState到底是同步还是异步呢

先来思考一个陈词滥调的问题,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 的 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 等)
  2. setState 和 useState 是异步执行的(不会立刻更新 state 的后果)
  3. 屡次执行 setState 和 useState,只会调用一次从新渲染 render
  4. 不同的是,setState 会进行 state 的合并,而 useState 则不会
  5. 在 setTimeout,Promise.then 等异步事件中
  6. setState 和 useState 是同步执行的(立刻更新 state 的后果)
  7. 屡次执行 setState 和 useState,每一次的执行 setState 和 useState,都会调用一次 render

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

退出移动版