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

Краткий обзор

В прошлой статье мы обсуждали, что параллельный режим предоставляет следующие возможности:

  1. Повышенная производительность
  2. Новые шаблоны пользовательского интерфейса

Теперь давайте посмотрим, как именно это происходит. Для начала давайте сначала рассмотрим проблемы.

Примечание. Может показаться, что все усложняется. Мне лично было немного трудно разобраться в вещах. Совет — просто понять, что происходит. Вы должны иметь возможность видеть все, что происходит. Не беспокойтесь о том, как обстоят дела. Мы расскажем об этом в следующих статьях.

Блокировка рендеринга

Проблема

Как мы знаем, как только обновление состояния начинает повторную визуализацию приложения, его нельзя остановить. Это может быть проблематично и отрицательно сказаться на пользовательском опыте и производительности. Давайте обсудим пример, приведенный в документации React.

Представьте себе длинный список предметов. Скажите, что список можно фильтровать. Это означает, что вы можете искать элементы в списке. Что делать, если этот список предметов очень длинный? Вы обнаружите, что приложение тормозит. Если быть точным, панель поиска заикается всякий раз, когда вы вводите в нее текст. Почему это происходит? (Вот пример того же)

Чтобы понять, что там происходит, рассмотрим, как обновляется состояние. Но перед этим немного об управляемых компонентах.

Контролируемые компоненты

Элемент ввода HTML поддерживает свое состояние внутри (вне React — в DOM). Это состояние доступно event.target.value внутри различных событий этого элемента ввода (event.target.value дает обновленное значение после того, как событие произошло). Поскольку элемент ввода поддерживает свое собственное состояние вне React, вам приходится заботиться о состояниях в разных местах: в вашем приложении React и в DOM. У React нет возможности получить доступ к состоянию DOM напрямую из ввода, поскольку поток данных однонаправленный — от родителя к дочерним элементам. Это означает, что внутри вашего приложения React вы не знаете о состоянии этого элемента ввода. Синхронизировать эти состояния невозможно. Чтобы решить эту проблему, React представил шаблон, называемый контролируемыми компонентами.

Для этого вы используете переменную состояния в своем приложении React. Эта переменная будет сопоставлена ​​с входными данными. В HTML свойство value ввода относится к его начальному значению. Но в React value prop относится к значению, которое должно отображаться на входе. Вместо этого начальное значение может быть установлено реквизитом defaultValue. Элемент ввода отображает любое значение, заданное ему в свойстве value. Таким образом, элемент ввода отображает значение, хранящееся в состоянии React, а не в его внутреннем состоянии (как уже говорилось, это значение передается элементу viavalue prop). Вы можете использовать входные события для обновления переменной состояния React. Пример ниже иллюстрирует этот процесс.

Рассмотрим контролируемый ввод текста. Давайте разберемся, что происходит, когда мы вводим в него:

  1. Изначально он отображает значение, указанное в value prop. Это значение, по сути, является переменной состояния, передаваемой на вход через value prop.
  2. Когда вы вводите в него символ, его внутреннее состояние обновляется. Он по-прежнему отображает значение из реквизита value. Но внутреннее состояние отражает новое значение.
  3. Вы получаете доступ к обновленному значению из onChange prop с помощью event.target.value и обновляете соответствующую переменную состояния вашего приложения с этим значением.
  4. Поскольку обновление состояния вызывает повторную визуализацию, входные данные визуализируются с новым значением переменной состояния. Таким образом, ввод отражает новое значение. Это делает состояние вашего приложения React единственным источником правды.

Ввод подключается к вашему приложению. Изменение ввода приведет к повторному рендерингу приложения с этим новым входным значением. Различные части вашего приложения могут зависеть от этого входного значения и перерисовываться при каждом изменении ввода. Вы можете использовать ввод декларативно в приложении.

Что произошло в примере с фильтруемым списком?

Приложение сохраняет условие поиска (значение ввода поиска) и список элементов, которые должны отображаться в состоянии. Следующие шаги выполняются всякий раз, когда вы вводите текст:

  1. Состояние приложения обновляется, чтобы отразить новое значение ввода текста. Приложение перерисовывается, отражая новое значение в текстовом вводе (управляемый ввод).
  2. Когда значение ввода текста изменяется, список фильтруется. Затем отфильтрованный список сохраняется в состоянии приложения. Компонент перерисовывается, показывая отфильтрованный список.
  3. Поскольку фильтрация списка стоит дорого, она занимает значительное время (чтобы смоделировать это, я запускаю очень длинный пустой цикл перед каждым обновлением списка). Обновление состояния, соответствующее фильтрации списка, требует времени. В течение этого времени состояние приложения не может быть изменено.
  4. Приложение заблокировано. Вы не можете вносить изменения в приложение во время обновления списка. Поэтому, если вы что-то набираете во вводе, он не обновляется сразу. Соответствующее обновление состояния ставится в очередь.
  5. Когда список обновляется, выполняется обновление в очереди. Таким образом, ввод обновляется. Поскольку обновление не было немедленным, казалось, что приложение «заикается».
  6. Поскольку входное значение было обновлено, список снова фильтруется — блокируя приложение. Это продолжается и продолжается, пока вы печатаете.
React.useEffect(
  () => { 
    setList(List.filter(item => { 
      if (searchTerm){
        let i = 0;
        while (i <= 10000000) i++;
        return item
               .title
               .toLowerCase()
               .includes(searchTerm.toLowerCase());      
      }
      else return true;
    })); 
  }, [searchTerm]);

Найдите минутку. Прочтите приведенные выше шаги еще раз, пока не увидите все ясно (вот исходный код, если хотите взглянуть). А теперь забавный эксперимент. Вы знаете, что такое параллельный режим, верно? Как параллельный режим может помочь нам здесь?

Обычно для решения этой проблемы люди используют одну из двух техник:

  1. Устранение неполадок: когда пользователь вводит текст, не обновляйте список сразу. Вместо этого подождите, пока пользователь закончит печатать, а затем обновите список. Но это может раздражать, так как список не обновляется по мере ввода.
  2. Дроссель: в качестве альтернативы вы можете обновить список. Но ограничьте частоту обновления. Таким образом, список будет обновляться только заданное количество раз в заданный интервал времени, а не при каждом нажатии клавиши.

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

Решение

Представьте, если бы вы работали в параллельном режиме. По сути, происходят два обновления состояния — обновление поля ввода и обновление списка. В параллельном режиме поток будет следующим:

  1. Вы вводите что-то в поле ввода. Это приводит к обновлению состояния (из-за изменения соответствующей переменной состояния) и, как результат, к повторному рендерингу приложения.
  2. Поскольку ввод был обновлен, список фильтруется в соответствии с новым значением. Обновление состояния списка стоит дорого. Но это не блокирует приложение. Это обновление отображается «в другой вселенной». Поскольку приложение не блокируется, вы можете свободно использовать ввод.
  3. Вы вводите что-то в поле ввода. Имейте в виду, что список все еще обновляется. Но поскольку это обновление находится в «другой вселенной», вы можете легко обновить свой ввод. Таким образом, обновление состояния ввода и обновление старого списка происходят одновременно.
  4. Вход обновляется до нового значения, в то время как происходит обновление старого списка. Обновление ввода запускает другое обновление списка. Старое обновление может продолжаться все это время.
  5. Новое обновление списка ставится в очередь, поскольку старое обновление все еще продолжается. Обратите внимание: поскольку мы обновляем одну и ту же переменную, мы делаем эти обновления по порядку, а не одновременно. Когда старое обновление готово, отображается соответствующий ему список. Новое обновление теперь начинает рендеринг.
  6. Это продолжается и продолжается. Вы можете обновлять список и ввод одновременно. Из-за этого вы можете изменить ввод во время обновления состояния списка. Приложение не тормозит и не тормозит.

Это может быть много, чтобы понять. Я предлагаю вам прочитать это снова, пока вы не поймете. Не волнуйтесь, если вы не понимаете этого прямо сейчас. В дальнейших статьях я продемонстрирую, как это происходит, на примере.

Улучшенная последовательность загрузки

Проблема

Большинство современных SPA (одностраничных приложений) имеют несколько экранов. Если бы вы реализовали это в React, вы бы предприняли следующие действия:

  1. Используйте переменную состояния, чтобы указать, какой экран отображать. Установите его начальное значение, соответствующее начальному экрану.
  2. Визуализируйте содержимое приложения условно. То есть проверьте значение этой переменной состояния и отобразите соответствующий экран.
  3. Если вы хотите перейти на новый экран, измените значение переменной состояния и установите его в соответствии с новым экраном.
  4. Это обновление состояния вызывает повторную визуализацию приложения. Как только это произойдет, старый экран исчезнет. Поскольку вы визуализируете содержимое экрана условно, отображается новый экран.

А теперь представьте, что новый экран еще не готов. Например, новый экран должен получать данные из API, прежде чем он сможет отображать разумный контент. В этом случае у вас нет старого экрана. И новый экран не может отображать осмысленный контент. Лучшее, что вы можете сделать, это показать состояние загрузки (например, счетчик). Но если экранов загрузки слишком много, это ухудшает пользовательский опыт. (Пример этого можно найти здесь)

Как параллельный режим может помочь вам в этом? Найдите минутку, чтобы подумать об этом (это сложно).

Решение

В параллельном режиме произойдет следующее:

  1. Вы хотите перейти к новому экрану. Таким образом, вы запускаете обновление состояния для нового экрана. Но это обновление происходит в «другой вселенной».
  2. Вы не теряете старое состояние. Таким образом, вы остаетесь на текущем экране. Вы можете показать индикатор загрузки, чтобы показать, что обновление происходит в «другой вселенной». Помните, что вам не обязательно понимать, как все происходит. Просто поймите, что сейчас происходит.
  3. Когда новый экран загрузит достаточно данных для отображения значимого содержимого, вы позволяете обновлению состояния использовать DOM и визуализировать новый экран.

Это намного лучший UX. Хотите увидеть нечто подобное в действии? Перейдите в раздел Документация React. Используя инструменты разработчика вашего браузера, отключите кэш и скорость сети, чтобы имитировать медленную сеть. Перезагрузите страницу. Теперь, когда вы переходите с одной ссылки на другую, вы видите индикатор загрузки в правом верхнем углу. Новый экран загружается, когда он готов.

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

Как это все укладывается?

Мы видели два (абстрактных) примера параллельного режима. А пока давайте посмотрим, как наше понимание параллельного режима вписывается в приведенное выше обсуждение. В параллельном режиме React может отображать обновления одновременно — в асинхронном режиме. Это означает, что несколько обновлений происходят одновременно. Немного синхронизировав, мы можем контролировать отображение обновлений. Мы увидим, как это сделать, в следующих статьях, когда будем реализовывать параллельный режим.

Параллельный режим просто означает возможность отображать несколько обновлений вместе, а затем при необходимости фиксировать их в DOM.

Имея это в виду, рассмотрим первый пример. Если происходит обновление и поступает другое, более «срочное» обновление (обновления, основанные на взаимодействии), мы можем позволить срочному обновлению зафиксироваться первым и отложить текущее обновление. Мы видели, насколько важным это может быть в примере с фильтруемым списком.

Давайте теперь посмотрим на второй пример. Когда мы хотим перейти на новый экран, мы начинаем рендерить соответствующее ему обновление в памяти. Поскольку обновление асинхронное, текущее состояние не теряется. Мы можем остаться на текущем экране и дождаться завершения обновления. Когда следующий экран будет готов, мы можем зафиксировать его в DOM, что приведет к отрисовке нового экрана на экране. Фиксация обновления фактически задерживается.

Глядя на эти примеры и определение, упомянутое в начале этого раздела, все понятно. Чтобы процитировать React docs,

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

Это пока все. В следующей статье мы рассмотрим все новые функции Suspense и улучшения, которые они привносят. А пока мир; )