Вы действительно понимаете setState?

внешний интерфейс JavaScript React.js
Вы действительно понимаете setState?

Интервьюер: «В ответsetStateЭто синхронно или асинхронно? "
я: "Асинхронный,setStateНевозможно получить результат сразу. "

Интервьюер: «В каком сценарии он асинхронный, но может ли он быть синхронным, и в каком сценарии он синхронный?»
Я:"......"

setStateЭто действительно асинхронно?

Я только смотрел на это в течение последних двух днейsetStateЧасть кода реализации, здесь я выскажу свое личное мнение, может быть больше текста или картинок, нетерпеливые студенты могут пропустить, чтобы увидеть резюме (Исходная версия 16.4.1).

Прежде чем смотреть на это, чтобы облегчить понимание и упростить процесс, мы по умолчанию используем внутренний код реакции на выполнение.performWork,performWorkOnRoot,performSyncWork,performAsyncWorkКогда эти четыре метода есть, React обновляется и воздействует на пользовательский интерфейс.

Синтетическое событиеsetState

Прежде всего, вы должны понимать, что такое синтетические события.Чтобы решить проблемы с кроссплатформенностью и совместимостью, react инкапсулирует набор механизмов событий сам по себе для проксирования нативных событий, таких как вjsxобщий вonClick,onChangeЭто синтетические события.

class App extends Component {

  state = { val: 0 }

  increment = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 输出的是更新前的val --> 0
  }
  render() {
    return (
      <div onClick={this.increment}>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

в синтетических событияхsetStateМетод записи более распространен, щелкните событие, чтобы изменитьthis.state.valгосударственная стоимостьincrementВы можете увидеть стек вызовов, нажав точку останова в событии.Здесь я размещаю блок-схему, которую я нарисовал:

合成事件中setState的调用栈
отdispatchInteractiveEventприбытьcallCallBackПока это обработка и выполнение синтетических событий, начиная сsetStateприбытьrequestWorkэто вызовthis.setStateЛогика здесь в основном выглядитrequestWorkЭта функция (изdispatchEventприбытьrequestWorkСтек вызовов принадлежитinteractiveUpdates$1изtryкодовый блок, упомянутый ниже).

function requestWork(root, expirationTime) {
  addRootToSchedule(root, expirationTime);

  if (isRendering) {
    // Prevent reentrancy. Remaining work will be scheduled at the end of
    // the currently rendering batch.
    return;
  }

  if (isBatchingUpdates) {
    // Flush work at the end of the batch.
    if (isUnbatchingUpdates) {
      // ...unless we're inside unbatchedUpdates, in which case we should
      // flush it now.
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, false);
    }
    return;
  }

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpiration(expirationTime);
  }
}

существуетrequestWorkВ трех ветвях есть три ветви if и два метода.performWorkOnRootа такжеperformSyncWork, что является нашей функцией обновления по умолчанию, но в синтетическом событии берется вторая ветка if, а во второй ветке два признакаisBatchingUpdatesа такжеisUnbatchingUpdatesОба начальных значенияfalse,Но когдаinteractiveUpdates$1ЦКisBatchingUpdatesустановить какtrue, следующееinteractiveUpdates$1код:

function interactiveUpdates$1(fn, a, b) {
  if (isBatchingInteractiveUpdates) {
    return fn(a, b);
  }
  // If there are any pending interactive updates, synchronously flush them.
  // This needs to happen before we read any handlers, because the effect of
  // the previous event may influence which handlers are called during
  // this event.
  if (!isBatchingUpdates && !isRendering && lowestPendingInteractiveExpirationTime !== NoWork) {
    // Synchronously flush pending interactive updates.
    performWork(lowestPendingInteractiveExpirationTime, false, null);
    lowestPendingInteractiveExpirationTime = NoWork;
  }
  var previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
  var previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingInteractiveUpdates = true;
  isBatchingUpdates = true;  // 把requestWork中的isBatchingUpdates标识改为true
  try {
    return fn(a, b);
  } finally {
    isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

в этом методеisBatchingUpdatesустановлен вtrue, в результате чегоrequestWorkметод,isBatchingUpdatesдляtrue,ноisUnbatchingUpdatesдаfalse, и был возвращен непосредственно.

Куда уходит логика возврата?Вернулась наконец.interactiveUpdatesЭтот метод, присмотритесь, этот метод имеетtry finallyГрамматика, front-end студенты на самом деле используются меньше, короче, они будут выполняться в первую очередьtryоператоры в блоке кода перед выполнениемfinallyкод в , покаfn(a, b)находится в блоке кода попытки, только что упомянутом вrequestWorkИменно эта fn возвращается в (упомянутом выше从dispatchEventприбытьrequestWorkвесь стек вызовов).

так что когда тыincrementвызыватьsetStateКогда я позже перехожу к console.log, он принадлежитtryВыполнение в блоке кода, но поскольку это синтетическое событие, состояние блока кода попытки не обновляется после выполнения блока кода, поэтому введенный вами результат является тем, который был до обновления.stateЗначение, это привело к так называемому «асинхронному», но когда выполняется ваш блок Try Code (то есть вашего приращения синтетического события), на этот раз будет выполненоfinallyкод вfinallyказнен вperformSyncWorkметод, на этот раз обновит вашstateи отображается в пользовательском интерфейсе.

Во-вторых, в функции жизненного циклаsetState

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    this.setState({ val: this.state.val + 1 })
   console.log(this.state.val) // 输出的还是更新前的值 --> 0
 }
  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

Стек вызовов setState в функции ловушки:

По сути, это все равно что синтетические события, когдаcomponentDidmountПри выполнении внутри реакции нет обновления, после выполненияcomponentDidmountиди позжеcommitUpdateQueueвозобновить. Это приводит вас кcomponentDidmountсерединаsetStateПосле перехода в console.log результатом остается значение до обновления.

3. В исходном событииsetState

class App extends Component {

  state = { val: 0 }

  changeValue = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 输出的是更新后的值 --> 1
  }

 componentDidMount() {
    document.body.addEventListener('click', this.changeValue, false)
 }
 
  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

Нативные события относятся к нереагирующим синтетическим событиям, нативным прослушивателям событий.addEventListener, или вы можете использовать нативные js, jq напрямуюdocument.querySelector().onclickЭта форма событий привязки является собственным событием.

Стек вызовов нативного события относительно прост, потому что нет такого понятия, как множество синтетических событий, событие щелчка запускается напрямую, аrequestWork,существуетrequestWorkиз-заexpirationTime === SyncПричина, только что ушлаperformSyncWorkОбновление не похоже на возврат в синтетическом событии или функции-ловушке, поэтому, когда вы устанавливаетеState в нативном событии, вы можете синхронно получать обновленное значение состояния.

В-четвертых, в setTimeoutsetState

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val) // 输出更新后的值 --> 1
    }, 0)
 }

  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

существуетsetTimeoutвходитьsetStateЭто не одна сцена, вам решать, потому что вы можетеsetTimeout, может быть в функции ловушкиsetTimeout, также в нативном событииsetTimeout, но какой бы сценарий это ни был, исходя изevent loopпод модель,setTimeoutидти посерединеsetStateВсегда получайте последнее значение состояния.

Возьмите каштан, как в предыдущем синтетическом событии, так как выsetTimeout(_ => { this.setState()}, 0)вtryкодовый блок, когда выtryблок кода выполняется дляsetTimeout, кинуть в очередь, и не выполнять, а выполнить сначалаfinallyКодовый блок и т.д.finallyзаконченный,isBatchingUpdatesснова сталfalse, что приводит к окончательному выполнению очередиsetStateкогда,requestWorkХодьба такая же, как родное событиеexpirationTime === SyncЕсли ветвь, то производительность будет такой же, как и у собственного события, а самое последнее значение состояния можно будет получить синхронно.

Пятерки,setStateМассовое обновление в

class App extends Component {

  state = { val: 0 }

  batchUpdates = () => {
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
 }

  render() {
    return (
      <div onClick={this.batchUpdates}>
        {`Counter is ${this.state.val}`} // 1
      </div>
    )
  }
}

Результат выше заканчивается равным 1, вsetStateКогда реакция создастupdateQueue,пройти черезfirstUpdate,lastUpdate,lastUpdate.nextподдерживать очередь обновлений, в финалеperformWork, тот же ключ будет перезаписан, только последнийsetStateЧтобы обновить, вот часть кода реализации:

function createUpdateQueue(baseState) {
  var queue = {
    expirationTime: NoWork,
    baseState: baseState,
    firstUpdate: null,
    lastUpdate: null,
    firstCapturedUpdate: null,
    lastCapturedUpdate: null,
    firstEffect: null,
    lastEffect: null,
    firstCapturedEffect: null,
    lastCapturedEffect: null
  };
  return queue;
}

function appendUpdateToQueue(queue, update, expirationTime) {
  // Append the update to the end of the list.
  if (queue.lastUpdate === null) {
    // Queue is empty
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
  if (queue.expirationTime === NoWork || queue.expirationTime > expirationTime) {
    // The incoming update has the earliest expiration of any update in the
    // queue. Update the queue's expiration time.
    queue.expirationTime = expirationTime;
  }
}

взгляните 🌰

class App extends React.Component {
  state = { val: 0 }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val);

      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
    }, 0)
  }

  render() {
    return <div>{this.state.val}</div>
  }
}

В сочетании с приведенным выше анализом в функции ловушкиsetStateОбновленное значение не может быть получено немедленно, поэтому 0 выводится в течение первых двух раз. Когда выполнение достигаетsetTimeoutПри входе значения двух предыдущих состояний были обновлены, т.к.setStateстратегия пакетного обновления,this.state.valДействителен только в последний раз, он равен 1, а вsetTimmoutсерединаsetStateРезультат обновления можно получить синхронно, поэтомуsetTimeoutДва раза в выводе 2, 3 конечный результат0, 0, 2, 3.

Суммировать :

  1. setState"Асинхронный" только в синтетических событиях и хуках, в нативных событиях иsetTimeoutсинхронизированы.
  2. setState«Асинхронность» не означает, что внутренняя реализация реализована асинхронным кодом, на самом деле процесс и код, исполняемый сам по себе, являются синхронными, но последовательность вызова синтетических событий и функций-ловушек предшествует обновлению, так что синтетические события а хуки-функции сразу получить нельзя.К обновленному значению формируется так называемая "асинхронность".Конечно, обновленный результат можно получить через callback во втором параметре setState(partialState, callback).
  3. setStateОптимизация пакетного обновления также основана на «асинхронности» (синтетические события, функции ловушек) и не будет обновляться пакетами в собственных событиях и setTimeout.В «асинхронном», если одно и то же значение повторяется несколько разsetState,setStateСтратегия пакетного обновления перезапишет его, возьмет последнее выполнение, если оно одновременноsetStateНесколько отдельных значений, которые объединяются и обновляются пакетами при обновлении.

Вышеизложенное является моим поверхностным пониманием после прочтения части кода, а анализ деталей исходного кода меньше, в основном, чтобы все понялиsetStateКакой процесс и результаты произошли в разных сценариях и разных методах написания, я надеюсь помочь всем, потому что это личное понимание и мнение, если что-то не так, пожалуйста, укажите и обсудите вместе.

Кроме того, реклама для друга:

Друг организовал волну вступительных постов и разместил их наgithub, желающие могут связаться с cXE3MjcwNDAxNDE=