Реагировать на рендеринг крюка в дополнительное время - PullRequest
0 голосов
/ 15 февраля 2019

Мой код вызывает неожиданное количество повторных визуализаций.

function App() {    
    const [isOn, setIsOn] = useState(false)
    const [timer, setTimer] = useState(0)
    console.log('re-rendered', timer)

    useEffect(() => {
        let interval

        if (isOn) {
            interval = setInterval(() => setTimer(timer + 1), 1000)
        }

        return () => clearInterval(interval)
    }, [isOn])

    return (
      <div>
        {timer}
        {!isOn && (
          <button type="button" onClick={() => setIsOn(true)}>
            Start
          </button>
        )}

        {isOn && (
          <button type="button" onClick={() => setIsOn(false)}>
            Stop
          </button>
        )}
      </div>
    );
 }

Обратите внимание на console.log в строке 4. Я ожидал, что будет выведено следующее:

повторно обработано 0

повторно обработано 0

повторно обработано 1

Первый журнал предназначен для начального отображения.Второй журнал предназначен для повторного рендеринга, когда состояние «isOn» изменяется нажатием кнопки.Третий журнал - это когда setInterval вызывает setTimer, поэтому он снова рендерится.Вот что я на самом деле получаю:

повторно отображено 0

повторно отображено 0

повторно отображено 1

повторно отображено 1

Я не могу понять, почему существует четвертый журнал.Вот ссылка на REPL этого:

https://codesandbox.io/s/kx393n58r7

*** Просто чтобы уточнить, я знаю, что решение состоит в использовании setTimer (timer => timer + 1), но яхотел бы знать, почему приведенный выше код вызывает четвертый рендер.

1 Ответ

0 голосов
/ 15 февраля 2019

Функция с большей частью того, что происходит, когда вы вызываете установщик, возвращаемый useState, имеет значение dispatchAction в ReactFiberHooks.js (в настоящее время начинается со строки 1009).

Блок кода, который проверяет, изменилось ли состояние (и, возможно, пропускает повторную визуализацию, если она не изменилась), в настоящее время окружен следующим условием:

if (
  fiber.expirationTime === NoWork &&
  (alternate === null || alternate.expirationTime === NoWork)
) {

Мое предположение при просмотре этого состояло в том, чтоэто условие оценивается как ложное после второго setTimer вызова.Чтобы убедиться в этом, я скопировал файлы разработки CDN React и добавил некоторые журналы консоли в функцию dispatchAction:

function dispatchAction(fiber, queue, action) {
  !(numberOfReRenders < RE_RENDER_LIMIT) ? invariant(false, 'Too many re-renders. React limits the number of renders to prevent an infinite loop.') : void 0;

  {
    !(arguments.length <= 3) ? warning$1(false, "State updates from the useState() and useReducer() Hooks don't support the " + 'second callback argument. To execute a side effect after ' + 'rendering, declare it in the component body with useEffect().') : void 0;
  }
  console.log("dispatchAction1");
  var alternate = fiber.alternate;
  if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
    // This is a render phase update. Stash it in a lazily-created map of
    // queue -> linked list of updates. After this render pass, we'll restart
    // and apply the stashed updates on top of the work-in-progress hook.
    didScheduleRenderPhaseUpdate = true;
    var update = {
      expirationTime: renderExpirationTime,
      action: action,
      eagerReducer: null,
      eagerState: null,
      next: null
    };
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      var lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  } else {
    flushPassiveEffects();

    console.log("dispatchAction2");
    var currentTime = requestCurrentTime();
    var _expirationTime = computeExpirationForFiber(currentTime, fiber);

    var _update2 = {
      expirationTime: _expirationTime,
      action: action,
      eagerReducer: null,
      eagerState: null,
      next: null
    };

    // Append the update to the end of the list.
    var _last = queue.last;
    if (_last === null) {
      // This is the first update. Create a circular list.
      _update2.next = _update2;
    } else {
      var first = _last.next;
      if (first !== null) {
        // Still circular.
        _update2.next = first;
      }
      _last.next = _update2;
    }
    queue.last = _update2;

    console.log("expiration: " + fiber.expirationTime);
    if (alternate) {
      console.log("alternate expiration: " + alternate.expirationTime);
    }
    if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
      console.log("dispatchAction3");

      // The queue is currently empty, which means we can eagerly compute the
      // next state before entering the render phase. If the new state is the
      // same as the current state, we may be able to bail out entirely.
      var _eagerReducer = queue.eagerReducer;
      if (_eagerReducer !== null) {
        var prevDispatcher = void 0;
        {
          prevDispatcher = ReactCurrentDispatcher$1.current;
          ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          var currentState = queue.eagerState;
          var _eagerState = _eagerReducer(currentState, action);
          // Stash the eagerly computed state, and the reducer used to compute
          // it, on the update object. If the reducer hasn't changed by the
          // time we enter the render phase, then the eager state can be used
          // without calling the reducer again.
          _update2.eagerReducer = _eagerReducer;
          _update2.eagerState = _eagerState;
          if (is(_eagerState, currentState)) {
            // Fast path. We can bail out without scheduling React to re-render.
            // It's still possible that we'll need to rebase this update later,
            // if the component re-renders for a different reason and by that
            // time the reducer has changed.
            return;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          {
            ReactCurrentDispatcher$1.current = prevDispatcher;
          }
        }
      }
    }
    {
      if (shouldWarnForUnbatchedSetState === true) {
        warnIfNotCurrentlyBatchingInDev(fiber);
      }
    }
    scheduleWork(fiber, _expirationTime);
  }
}

, а вот вывод консоли с некоторыми дополнительными комментариями для ясности:

re-rendered 0 // initial render

dispatchAction1 // setIsOn
dispatchAction2
expiration: 0
dispatchAction3
re-rendered 0

dispatchAction1 // first call to setTimer
dispatchAction2
expiration: 1073741823
alternate expiration: 0
re-rendered 1

dispatchAction1 // second call to setTimer
dispatchAction2
expiration: 0
alternate expiration: 1073741823
re-rendered 1

dispatchAction1 // third and subsequent calls to setTimer all look like this
dispatchAction2
expiration: 0
alternate expiration: 0
dispatchAction3

NoWork имеет значение ноль.Вы можете видеть, что первый лог fiber.expirationTime после setTimer имеет ненулевое значение.В журналах второго вызова setTimer этот fiber.expirationTime был перемещен в alternate.expirationTime, что все еще не позволяет сравнивать состояние, поэтому повторная визуализация будет безусловной.После этого время истечения срока действия fiber и alternate равно 0 (NoWork), а затем выполняется сравнение состояний и предотвращается повторный рендеринг.

Это описание архитектуры React Fibre является хорошей отправной точкой для попытки понять цель expirationTime.

Наиболее важные части исходного кода для его понимания:

Я считаю, что время истечения в основном относится к параллельному режиму, который по умолчанию еще не включен.Время истечения указывает момент времени, после которого React будет принудительно совершать работу при первой же возможности.До этого момента React может выбрать пакетное обновление.У некоторых обновлений (например, из-за взаимодействия с пользователем) истекает очень короткий (высокий приоритет), а у других обновлений (например, из асинхронного кода после завершения выборки) истекает более длинный (низкий приоритет).Обновления, инициируемые setTimer из-за обратного вызова setInterval, попадают в категорию с низким приоритетом и могут быть пакетными (если включен режим одновременной работы).Поскольку существует вероятность того, что эта работа была пакетирована или потенциально отброшена, React ставит повторную визуализацию безоговорочно (даже если состояние не изменилось с момента предыдущего обновления), если предыдущее обновление имело expirationTime.

Вы можете увидеть мой ответ здесь , чтобы узнать немного больше о том, как пройти через код React, чтобы добраться до этой функции dispatchAction.

Для тех, кто хочет покопатьсяСобственно, вот CodeSandbox с моей модифицированной версией React:

Edit static

Файлы реагирования - это модифицированные копии этих файлов:

https://unpkg.com/react@16/umd/react.development.js
https://unpkg.com/react-dom@16/umd/react-dom.development.js
...