Являются ли React useState и setState синхронными или асинхронными?

React.js

Давайте сначала подумаем над общим вопросом,setState是同步还是异步?

Думай глубже,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>
  )
}

В заключение:

  • при нажатии同步执行кнопка, только повторно рендерить один раз
  • при нажатии异步执行Когда кнопка нажата, она отображается дважды

В случае синхронного и асинхронного выполнения одно и то же 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 выполняется дважды, но объединенный рендеринг выполняется один раз, печатая 3
  • при нажатии异步执行Когда кнопка нажата, каждый из двух setA рендерится один раз и печатает 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>
    )
  }
}
  • при нажатии同步执行кнопка, только повторно рендерить один раз
  • при нажатии异步执行Когда кнопка нажата, она отображается дважды

То же, что и результат 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>
    )
  }
}
  • при нажатии同步执行Когда кнопка нажата, два setStates объединяются, выполняется только последний раз, и печатается 2
  • при нажатии异步执行Когда кнопка нажата, каждый из двух setState будет отображаться один раз и печатать 2 и 3 соответственно.

这里跟useState不同,同步执行时useState也会对state进行逐个处理,而setState则只会处理最后一次

Почему между синхронным и асинхронным выполнением получаются разные результаты?

Это включает в себя механизм пакетного обновления реакции, который объединяет обновления.

  • Во-первых, зачем вам нужны обновления слияния?

Если обновление не будет объединено, компоненту придется выполнять повторный рендеринг каждый раз, когда выполняется useState, что приведет к недопустимому рендерингу и пустой трате времени (поскольку последний рендеринг перезапишет все предыдущие эффекты рендеринга). Таким образом, react будет собирать некоторые useState/setState, которые можно обновлять вместе, а также объединять и обновлять.

  • как слить обновление

Здесь react использует механизм транзакций.

Пакетное обновление в React реализовано через «Транзакцию». В части исходного кода React, посвященной транзакции, роль транзакции объясняется большим абзацем текста и изображением персонажа:

*                       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 не будет объединять состояние) обработка.

  • Почему setTimeout не может выполнять транзакционные операции

Из-за механизма делегирования событий в реакции событие, выполняемое путем вызова onClick, находится под контролем реакции.

И setTimeout находится вне контроля реакции, и реакция не может добавить логику транзакции до и после кода setTimeout (если только реакция не перезаписывает setTimeout).

Так что при встречеsetTimeout/setInterval/Promise.then(fn)/fetch 回调/xhr 网络回调Реакция вышла из-под контроля.

Соответствующий исходный код реакции выглядит следующим образом:

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()
}

executeContext представляет текущую стадию реакции, а NoContext можно понимать как состояние, в котором у реакции нет работы. И flushSyncCallbackQueue будет вызывать наш this.setState синхронно, а это значит, что наше состояние будет обновляться синхронно. Итак, мы знаем, что когда executeContext имеет значение NoContext, наш setState является синхронным.

Суммировать

Подведем итоги вышеописанных экспериментов:

  1. В обычном потоке событий реакции (например, onClick и т. д.)
  • setState и useState выполняются асинхронно (результат состояния не обновляется сразу)
  • Выполните setState и useState несколько раз, вызовите повторный рендеринг только один раз.
  • Разница в том, что setState объединит состояние, а useState — нет.
  1. в асинхронных событиях, таких как setTimeout, Promise.then и т. д.
  • setState и useState выполняются синхронно (результат немедленного обновления состояния)
  • Выполните setState и useState несколько раз, и каждый раз, когда выполняются setState и useState, рендеринг будет вызываться один раз.

Это немного сбивает с толку, просто напишите код и испытайте его на себе ~

использованная литература