React не обновляет DOM во время рендеринга, когда в `componentDidUpdate` есть долго работающая функция.

У меня следующая ситуация в интерфейсе настольной игры на основе React.

У меня есть обработчик событий, который обновляет состояние, когда игроки делают ход:

const move = ...
this.setState(function (state, _props) {
   return this.apply_move(state, move);
});

apply_move(state, move) {
   let board_copy = _.cloneDeep(state.board);
   // applies the move to board_copy somehow
   ...
   return {
     board: board_copy,
   };
}

Затем я хочу сначала визуализировать свое движение, а затем запустить ИИ, чтобы произвести движение для бота: вызов этой функции занимает несколько секунд, пока ИИ вычисляет свое движение. На самом деле это вызов функции WebAssembly.

Я помещаю запрос на перемещение ИИ в componentDidUpdate:

componentDidUpdate(prevProps, prevState) {
    if (this.state.history.length > prevState.history.length) {
      if (this.botHasToMove()) {
        this.getMoveFromBot();   // <--- this runs for 5 seconds
      }
    }
}

Я ожидаю, что это будет выполняться после вызова render, поэтому я сначала увижу, как отрисовывается мой ход, а затем будет пауза на пару секунд, пока ИИ вычисляет, а затем должен отрисовываться ход ИИ.

Однако проблема в том, что хотя я вижу, что render вызывается в консоли перед запросом перемещения ИИ, и я вижу, что состояние было обновлено с моим перемещением, но сама DOM не обновляется. Он обновляется только тогда, когда длинный вызов для получения движения ИИ завершен и происходит еще одно render. Затем оба движения отображаются одновременно, и я могу видеть их в браузере.

Я ожидал, что каждый вызов render должен обновлять DOM (если там что-то изменилось, а в моем случае это было), но по какой-то причине обновление DOM не происходит.

Если я не вызываю длинный вызов ИИ в componentDidUpdate, все работает нормально, и я вижу эффект от первого render.

Может кто-нибудь объяснить, почему это происходит, и научить меня, как правильно принудительно обновить DOM при первом вызове render?


person dying_sphynx    schedule 12.06.2020    source источник
comment
Вообще говоря, желание или потребность в принудительном обновлении является красным флагом, что ваш программный поток ошибочен. Если я вас правильно понял, вы хотите сделать перемещение и обновление состояния (и рендеринг), а затем позволить ИИ выполнить перемещение и обновление состояния (и рендеринг). Это правильно? Можете ли вы предоставить минимальный, полный и воспроизводимый пример кода вашего компонента?   -  person Drew Reese    schedule 13.06.2020
comment
Одним из способов избежать блокировки основного потока при расчете движения ИИ может быть использование WebWorker stackoverflow.com/questions/50731650/   -  person mzulch    schedule 13.06.2020
comment
Да, я думаю, что проблема здесь в том, что вызов долго работающей функции WebAssembly блокирует основной поток, и хотя render был вызван и завершен, в браузере не было изменений для фактического обновления DOM.   -  person dying_sphynx    schedule 13.06.2020
comment
Похоже, я смог отрендерить свой первый ход, используя setTimeout(() => this.getMoveFromBot(), 10); вместо this.getMoveFromBot() в componentDidUpdate выше. По-видимому, перерыв в 10 мс (или даже 1 мс) дает ему возможность обновить DOM.   -  person dying_sphynx    schedule 13.06.2020
comment
Похоже, что этот вопрос также актуален: stackoverflow.com/questions/41490837/   -  person dying_sphynx    schedule 13.06.2020
comment
Использование тайм-аута (даже с задержкой 0) работает, потому что оно перемещает результирующее обновление состояния в обратный вызов, помещенный в конец очереди событий, чтобы все обновления в очереди из текущего цикла рендеринга могли обрабатываться и повторно рендериться. Я уверен, что вы можете добиться того же более правильным образом, используя значение состояния, чья очередь это, и реализовывать каждый ход с помощью функции жизненного цикла componentDidUpdate.   -  person Drew Reese    schedule 14.06.2020
comment
@DrewReese, но, похоже, я уже сделал то, что вы предлагаете, то есть у меня есть функция this.botHasToMove(), которая проверяет, что вы называете, чья сейчас очередь (она проверяет логическое значение из this.state), и я помещаю запрос на ход ИИ в componentDidUpdate (как вы можете увидеть в исходном вопросе). Не совсем уверен, как это концептуально отличается от того, что вы предлагаете...   -  person dying_sphynx    schedule 14.06.2020
comment
@DrewReese Возможно, я не понимаю, так что именно вы подразумеваете под реализацией каждого хода с помощью функции componentDidUpdate? Ходы человека и ИИ сильно различаются: ходы человека создаются в обработчике событий, который просто обычно обновляет состояние. А повороты ИИ создаются длительным вызовом WebAssembly, который должен вызываться в какой-то момент сразу после получения и рендеринга движения человека.   -  person dying_sphynx    schedule 14.06.2020


Ответы (1)


Мне нравятся вопросы, которые вы задали, потому что мне нравятся такого рода проблемы, вы на самом деле отлично справляетесь со своим кодом, но есть проблема с вашим кодом, вы на самом деле упоминаете длинный вызов или длинный код выполнения, JS запускается в одном потоке, это означает, что все процессы выполняются только в одном канале (потоке), поэтому я настоятельно рекомендую вам посмотреть эти два видео о циклах событий в JS, это причина вашей проблемы:

Как работает Event Loops JS — JSConf.Asia

Как работает Event Loops JS — JSConf EU

краткий ответ на вашу проблему заключается в том, что вы блокируете выполнение потока в JS, я имею в виду, что все выполнение JS выполняется последовательно в «очереди выполнения», поэтому следующий код выполнения не будет выполняться, пока предыдущие не будут завершены.

Итак, как вы можете это исправить? чтобы избежать блокировки вашего потока, вам нужно выполнить свой код в другом потоке, но JS — это всего лишь язык выполнения одного потока, поэтому вам действительно нужно что-то под названием WebWorkers, они позволяют вам запускать код в другом "потоке", поэтому ваша проблема в том, что вы блокируете свою очередь выполнения из-за долгая функция выполнения занимает слишком много времени для разрешения.

Я надеюсь, что это может вам очень помочь и помочь вам понять, как работают циклы JS Event/очередь выполнения.

person Juorder Gonzalez    schedule 13.06.2020