Первый предварительный просмотр React Concurrent Mode Part 2: Parallel World of useTransition

внешний интерфейс JavaScript React.js
Первый предварительный просмотр React Concurrent Mode Part 2: Parallel World of useTransition

Предыдущая статья представилаSuspense, то эта статья расскажет о его хорошем партнереuseTransition. Если вы поклонник React, эти две статьи нельзя пропустить.

Мы знаем, что React претерпел потрясающую внутреннюю оптимизацию, а некоторые компактные новые API также были предоставлены извне.Эти API в основном используются для оптимизации взаимодействия с пользователем.. React официально использует очень длинный документ«Параллельные шаблоны пользовательского интерфейса»Специально представить мотивацию и создание этого аспекта, главным героем которого являетсяuseTransition.


Статьи по Теме



План этой статьи


React использует’Параллельная вселенная'Для сравнения useTransition API. какие?

Это будет лучше понятно, если использовать ветку Git в качестве аналогии, как показано на рисунке ниже, React может просматривать текущее представление (которое можно рассматривать какMaster) в веткеForkиз новой ветки (также называемойPending), обновить эту новую ветку, аMasterОставаясь отзывчивыми и обновляемыми, эти две ветки подобны «параллельным вселенным», где они не мешают друг другу. когдаPendingВетка готова «сделана», затем объединена (зафиксирована) вMasterфилиал.


useTransitionКак временной туннель, позволяющий компонентам войти в параллельную вселенную и ждать в этой параллельной вселенной.异步状态(асинхронный запрос, задержка, что угодно) готов. Конечно, компоненты нельзя бесконечно ожидать в параллельных вселенных,useTranstionВы можете настроить период тайм-аута.异步状态Не готовы также будут вынуждены вернуться в реальный мир. Вернувшись в реальный мир, React немедленно объединит изменения компонента Pengding и представит их пользователю.

Таким образом, вы можете думать о компонентах React как о трех состояниях в параллельном режиме:


  • Normal- Компоненты в нормальном состоянии
  • Suspense- Компонент приостановлен из-за асинхронного состояния
  • Pending- Компоненты для входа в параллельные вселенные. Соответственно, есть также ожидающие «изменения состояния», эти изменения не сразу отправляются в пользовательский интерфейс React, а кэшируются, ожидая готовности Suspense или истечения времени ожидания.

Возможно, вы еще не совсем поняли это, ничего страшного, продолжайте читать.



Каков сценарий применения?

Какая польза от «параллельных вселенных»? Мы не говорим о вещах на уровне кода или архитектуры. сингл отUIПогляди:В некоторых сценариях взаимодействия с пользовательским интерфейсом мы не хотим сразу применять изменения к странице..

🔴Например, если вы переключаетесь с одной страницы на другую, загрузка новой страницы может занять некоторое время. На самом деле, мы предпочитаем оставаться на предыдущей странице некоторое время и сохранять реакцию на некоторые операции. Например, мы можем отменить, или выполнить другие операции, и дать я вижу пустую страницу без ничего или значок состояния загрузки в режиме ожидания, и я чувствую, что я делаю ненужное ожидание.

Такой сценарий взаимодействия на самом деле очень распространен.Прямым примером является браузер:


Притворись, что я могу позволить себе AirPods


И наш часто используемый Github:

Известный зарубежный сайт знакомств


Например, я хочу нажать, чтобы купитьAirPods, браузер останется на предыдущей странице до тех пор, пока не будет получен ответ на запрос следующей страницы или пока не истечет время ожидания. Кроме того, браузер подскажет статус запроса через индикатор загрузки в адресной строке. Такой дизайн взаимодействия намного лучше, чем переключение и показ пустой страницы: страница может оставаться отзывчивой для пользователя, а запрос можно отменить в любой момент и остаться на исходной странице.

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

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

Кроме того,🔴useTransition также можно использовать для переноса низкоприоритетных обновлений.. В текущей ситуации React не хочет раскрывать слишком много низкоуровневых деталей режима Concurrent. Используйте useTransition только в том случае, если вы хотите запланировать обновления с низким приоритетом.



дебюты useTransition


Как показано выше, сначала мы определяем различные состояния страницы, как описано в официальной документации React.В нем упоминается, что загрузка страницы состоит из следующих трех этапов.:

① Переход

Относится к стадии, когда страница не готова, ожидая загрузки важных данных. В соответствии с различными стратегиями отображения страница может находиться в следующих двух состояниях:

  • ⚛️Отступил. Немедленно переключайте страницы, показывая большой индикатор загрузки или пустую страницу. Что означает «вырождение»?Согласно React, страница, которая изначально имела контент, а теперь не имеет контента, является своего рода вырождением или исторической «регрессией».

  • ⚛️ В ожидании. ЭтоuseTransitionДостигаемое состояние, т. е. оставаться на текущей странице и поддерживать отклик текущей страницы. существуетКлючевые данные готовывходитьSkeleton(экран скелета) или подождите, пока тайм-аут не вернется кRecededгосударство.


② Стадия загрузки (Загрузка)

Относится关键数据Все готово для отображения скелета или рамки страницы. Этот этап имеет статус:

  • ⚛️ Скелет. Ключевые данные были загружены, и на странице показана рамка основного тела.

③ этап готовности (Готово).

Это означает, что страница полностью загружена. Этот этап имеет статус:

  • ⚛️ЗавершитьСтраница полностью отображается


В традиционном React, когда мы меняем состояние, чтобы перейти на новый экран, мы получаем🔴Receded -> Skeleton -> Completeдорожка. достичь до🔴Pending -> Skeleton -> CompleteЭтот путь загрузки является более сложным.useTransitionможет изменить эту ситуацию.


Далее просто смоделируйте переключение страниц, сначала посмотрите, как оно загружается по умолчанию:

function A() {
  return <div className="letter">A</div>;
}

function B() {
  // ⚛️ 延迟加载2s,模拟异步数据请求
  delay("B", 2000);
  return <div className="letter">B</div>;
}

function C() {
  // ⚛️ 延迟加载4s,模拟异步数据请求
  delay("C", 4000);
  return <div className="letter">C</div>;
}

// 页面1
function Page1() {
  return <A />;
}

// 页面2
function Page2() {
  return (
    <>
      <B />
      <Suspense fallback={<div>Loading... C</div>}>
        <C />
      </Suspense>
    </>
  );
}

function App() {
  const [showPage2, setShowPage2] = useState(false);

  // 点击切换到页面2
  const handleClick = () =>  setShowPage2(true)

  return (
    <div className="App">
      <div>
        <button onClick={handleClick}>切换</button>
      </div>
      <div className="page">
        <Suspense fallback={<div>Loading ...</div>}>
          {!showPage2 ? <Page1 /> : <Page2 />}
        </Suspense>
      </div>
    </div>
  );
}

Взгляните на беговой эффект:

После нажатия на тумблер мы сразу увидим большоеLoading..., то B загружается через 2 с, а C загружается через 2 с. Этот процессReceded -> Skeleton -> Complete


Теперь, пожалуйста, используйте Transition здесь 🎉, просто модифицируя приведенный выше код:

// ⚛️ 导入 useTransition
import React, { Suspense, useState, useTransition } from "react";

function App() {
  const [showPage2, setShowPage2] = useState(false);
  // ⚛️ useTransition 接收一个超时时间,返回一个startTransition 函数,以及一个 pending
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  const handleClick = () =>
    // ⚛️ 将可能触发 Suspense 挂起的状态变更包裹在 startTransition 中
    startTransition(() => {
      setShowPage2(true);
    });

  return (
    <div className="App">
      <div>
        <button onClick={handleClick}>切换</button>
        {/* ⚛️ pending 表示处于待定状态, 你可以进行一些轻微的提示 */}
        {pending && <span>切换中...</span>}
      </div>
      <div className="page">
        <Suspense fallback={<div>Loading ...</div>}>
          {!showPage2 ? <Page1 /> : <Page2 />}
        </Suspense>
      </div>
    </div>
  );
}

API useTransition Hook относительно прост и состоит из 4 ключевых моментов:

  • timeoutMs, указывает период ожидания переключения (самый длинный в параллельной вселенной), useTransition будет удерживать React на текущей странице до тех пор, пока он не будет активирован. Suspence не будет готов или не истечет время ожидания.

  • startTransition, переносите изменения состояния, которые могут вызвать переключение страниц (строго говоря, вызвать зависание приостановки) вstartTransitionЗатем фактически startTransition предоставляет «обновленный контекст». Мы углубимся в детали в следующем разделе

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

  • Suspense, useTransition должен взаимодействовать с Suspense для реализации переходного состояния, то естьstartTransitionОбновления должны вызвать приостановку приостановки.


Взгляните на реальный эффект от операции!


можно найти в этомCodeSandboxПроверьте беговой эффект

Эффект точно такой же, как у «первого изображения» в начале этого раздела: React останется на текущей странице,pendingСтановится истинным, тогда B готов первым, и тут же переключается интерфейс. Весь процесс соответствуетPending -> Skeleton -> Completeмаршрут из.

startTransitionсередина变更после запускаSuspense, Реакция будет变更Отмеченный в состоянии «Ожидание», React отложит «фиксацию» этих изменений. такНа самом деле никакой параллельной вселенной, о которой говорилось в начале, нет, такой высокой и волшебной, React просто задержал подачу этих изменений. Все, что мы видим в интерфейсе, — это старое или незавершенное состояние, предварительный рендеринг React в фоновом режиме..

Обратите внимание, что React пока не отправлял эти изменения. Это не означает, что React «застрял». Компоненты в состоянии «Ожидание» также получат ответ пользователя и внесут новые изменения состояния. Обновление нового состояния также может перезаписать или завершиться. Состояние ожидания.


Подытожим условия входа и выхода из состояния ожидания:

  • Введите ожиданиеГосударство в первую очередь должно быть状态变更завернут вstartTransition, и эти обновления приводят к зависанию приостановки
  • Выход в ожиданииСуществует три варианта статуса: ① Приостановка готова; ② Тайм-аут; ③ Перезаписано или прекращено новым обновлением статуса.


Предварительное исследование принципа использования Transition

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

Предшественником useTransition былwithSuspenseConfig, SebmarkbageОдин упоминается в мае этого годаPRпредставил его.

А именно, он просто хочет настроить Suspense. Мы также можем проверить это с помощью последнего исходного кода. Работа useTransition «кажется» очень простой:

function updateTransition(
  config: SuspenseConfig | void | null,
): [(() => void) => void, boolean] {
  const [isPending, setPending] = updateState(false); // 相当于useState
  const startTransition = updateCallback(             // 相当于useCallback
    callback => {
      setPending(true); // 设置 pending 为 true
      // 以低优先级调度执行
      Scheduler.unstable_next(() => {
        // ⚛️ 设置suspenseConfig
        const previousConfig = ReactCurrentBatchConfig.suspense;
        ReactCurrentBatchConfig.suspense = config === undefined ? null : config;
        try {
          // 还原 pending
          setPending(false);

          // 执行你的回调
          callback();

        } finally {
          // ⚛️ 还原suspenseConfig
          ReactCurrentBatchConfig.suspense = previousConfig;
        }
      });
    },
    [config, isPending],
  );
  return [startTransition, isPending];
}

Это кажется очень распространенным, в чем смысл? Sebmarkbage также упомянул некоторую информацию в вышеупомянутом PR.

  • startTransition устанавливает ожидание в значение true, как только начинает выполняться. затем используйтеunstable_nextвыполнить обратный вызов,нестабильность_некст может понизить приоритет обновлений. То есть «изменение», инициированное в обратном вызове нестабильного_следующего, будет иметь более низкий приоритет и уступит место высокоприоритетному обновлению, или, когда текущая транзакция будет занята, оно будет запланировано для применения в следующем период простоя, но может быть применен и немедленно.

  • Суть в томReactCurrentBatchConfig.suspenseконфигурация, которая будет настраивать период ожидания приостановки.Это указывает на то, что изменения, вызванные этим интервалом, связаны с этимsuspenseConfig, эти изменения будут вычисляться самостоятельно в соответствии с suspenseConfigexpiredTime(Можно рассматривать как «приоритет»). Назовем эти изменения, связанные с suspenseConfig, какPending 变更.

  • Pending 变更Инициированные повторные рендеры (Render) также будут связаны сsuspenseConfig. Если приостановка срабатывает во время рендеринга, тоPending 变更Коммиты будут отложены, они будут кэшироваться в памяти и не будут принудительно отображаться в пользовательском интерфейсе до тех пор, пока не истечет время приостановки, не будет готова или не будет перезаписана другими обновлениями.

  • Pending 变更Он только задерживается и подается, но это не повлияет на согласованность конечных данных и представления. React будет повторно отображать в памяти, просто не отправляя его в пользовательский интерфейс.


Внутренняя реализация React настолько сложна, что мне дорого копаться в ней или выражать ее словами. Итак, по-другому, чтобы понять его поведение экспериментальным (черным ящиком) способом:

Эти экспериментальные коды находятся в этомCodeSandboxсередина


1️⃣ Используйте startTransition для запуска задач с низким приоритетом

Этот эксперимент в основном используется для проверкиunstable_next, это отменит приоритет обновления. С помощью следующих экспериментов мы будем наблюдать:startTransitionИзменения пакета будут обновлены чуть позже, когда задача будет занята, но финальное состояние останется прежним.

Экспериментальный код:

export default function App() {
  const [count, setCount] = useState(0);
  const [tick, setTick] = useState(0);
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  const handleClick = () => {
    // ⚛️ 同步更新
    setCount(count + 1);

    startTransition(() => {
      // ⚛️ 低优先级更新 tick
      setTick(t => t + 1);
    });
  };

  return (
    <div className="App">
      <h1>Hello useTransition</h1>
      <div>
        <button onClick={handleClick}>ADD + 1</button>
        {pending && <span>pending</span>}
      </div>
      <div>Count: {count}</div>
      {/* ⚛️ 这是一个复杂的组件,渲染需要一点时间,模拟繁忙的情况 */}
      <ComplexComponent value={tick} />
    </div>
  );
}

Результаты эксперимента следующие:


В случае непрерывных щелчковComplexComponentОбновления будут значительно отставать, потому что изменения тиков задерживаются и объединяются, но в итоге их результаты одинаковы.



2️⃣ Обновление startTransition вызывает приостановку

export default function App() {
  const [count, setCount] = useState(0);
  const [tick, setTick] = useState(0);
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  const handleClick = () => {
    startTransition(() => {
      setCount(c => c + 1);
      setTick(c => c + 1);
    });
  };

  return (
    <div className="App">
      <h1>Hello useTransition {tick}</h1>
      <div>
        <button onClick={handleClick}>ADD + 1</button>
        {pending && <span className="pending">pending</span>}
      </div>
      <Tick />
      <SuspenseBoundary id={count} />
    </div>
  );
}

const SuspenseBoundary = ({ id }) => {
  return (
    <Suspense fallback="Loading...">
      {/* 这里会抛出一个Promise异常,3s 后 resolved */}
      <ComponentThatThrowPromise id={id} />
    </Suspense>
  );
};

// Tick 组件每秒递增一次
const Tick = ({ duration = 1000 }) => {
  const [tick, setTick] = useState(0);

  useEffect(() => {
    const t = setInterval(() => {
      setTick(tick => tick + 1);
    }, duration);
    return () => clearInterval(t);
  }, [duration]);

  return <div className="tick">tick: {tick}</div>;
};

Когда мы нажимаем кнопку, счетчик и тик увеличиваются, и счетчик передается в SuspenseBoundary, который запускает Suspense.

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

Кроме того, вы обнаружите, что тик компонента App будет «остановлен», как SuspenseBoundary (см. тик после Hello Transition), потому что изменение тика также связано с suspenseConfig.

С другой стороны, компонент Tick увеличивается каждую секунду и не блокируется.

Это означает, что после запуска Suspense любые изменения, связанные с SuspenseConfig, будут «приостановлены».



3️⃣ Вывести обновление галочки из области startTransition

На основании 2️⃣ setTick упоминается вне рамок startTransition:

export default function App() {
  const [count, setCount] = useState(0);
  const [tick, setTick] = useState(0);
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  console.log("App rendering with", count, tick, pending);

  const handleClick = () => {
    setTick(c => c + 1);
    startTransition(() => {
      setCount(c => c + 1);
    });
  };

  const handleAddTick = () => setTick(c => c + 1);

  useEffect(() => {
    console.log("App committed with", count, tick, pending);
  });

  return (
    <div className="App">
      <h1>Hello useTransition {tick}</h1>
      <div>
        <button onClick={handleClick}>ADD + 1</button>
        <button onClick={handleAddTick}>Tick + 1</button>
        {pending && <span className="pending">pending</span>}
      </div>
      <Tick />
      <SuspenseBoundary id={count} />
    </div>
  );
}


Теперь галочка будет обновляться немедленно, а SuspenseBoundary все еще находится в ожидании.

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

App rendering with 1 2 true   # pending 被设置为true, count 这是时候是 1, 而 tick 是 2
App rendering with 1 2 true
read  1
App committed with 1 2 true    # 进入Pending 状态之前的一次提交,我们在这里开始展示 pending 指示符

# 下面 Tick 更新了三次(3s)
# 我们注意到,每一次 React 都会重新渲染一下 App 组件,即 'ping' 一下处于 Pending 状态的组件, 检查一下是否‘就绪’(没有触发Suspense)
# 如果还触发 Suspense, 说明还要继续等待,这些重新渲染的结果不会被提交

App rendering with 2 2 false # ping, 这里count变成了2,且 pending 变成了 false
App rendering with 2 2 false # 但是 React 在内存中渲染它们,我们看不到
read  2

Tick rendering with 76        # Tick 重新渲染
Tick rendering with 76
Tick committed with 76        # 提交 Tick 更新,刷新到界面上
App rendering with 2 2 false  # ping 还是没有就绪,继续 pending
App rendering with 2 2 false
read  2

Tick rendering with 77
Tick rendering with 77
Tick committed with 77
App rendering with 2 2 false # ping
App rendering with 2 2 false
read  2

Tick rendering with 78
Tick rendering with 78
Tick committed with 78
App rendering with 2 2 false # ping
App rendering with 2 2 false
read  2

# Ok, Promise 已经就绪了,这时候再一次重新渲染 App
# 这次没有触发 Suspense,React 会马上提交用户界面
App rendering with 2 2 false
App rendering with 2 2 false
read  2
App committed with 2 2 false

Из приведенного выше журнала мы можем четко понять поведение обновления компонента Pending.



4️⃣ Вложенная приостановка

На основе 3️⃣ переписать SuspenseBoundary на DoubleSuspenseBoundary, где Suspense будет вложен для загрузки более трудоемкого ресурса:

const DoubleSuspenseBoundary = ({ id }) => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      {/* 需要加载 2s  */}
      <ComponentThatThrowPromise id={id} timeout={2000} />
      <Suspense fallback={<div>Loading second...</div>}>
        {/* 需要加载 4s  */}
        <ComponentThatThrowPromise id={id + "second"} timeout={4000} />
      </Suspense>
    </Suspense>
  )
}

Проверьте эффект:


Во-первых, обратите внимание на первое крепление,Приостановка не запускает отложенную фиксацию при первом монтировании, поэтому мы сначала видимLoading..., то первыйComponentThatThrowPromiseПосле загрузки отображатьComponentThatThrowPromise id: 0иLoading second..., и, наконец, полностью загружены.

Затем нажимаем кнопку, в это время DoubleSuspenseBoundary будет стоять на месте, ждем 5с (то есть второйComponentThatThrowPromiseзагружен) перед отправкой.


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

Чувствуете себя немного не так? Я долго думал об этом здесь, на официальном документеConcurrent UI Patterns (Experimental) - Wrap Lazy Features in <Suspense>сказал, второйComponentThatThrowPromiseуже вложены вSuspenseТеоретически он не должен блокировать коммиты.

Вернемся к первому предложению в начале: 'Приостановка не запускает отложенную фиксацию при первом монтировании'. Давайте попробуем еще раз, установим ключ на DoubleSuspenseBoundary, чтобы заставить его уничтожить и воссоздать:

export default function App() {
  // .....
  return (
    <div className="App">
      <h1>Hello useTransition {tick}</h1>
      <div>
        <button onClick={handleClick}>ADD + 1</button>
        {pending && <span className="pending">pending</span>}
      </div>
      <Tick />
      {/* ⚛️ 这里添加key,强制重新销毁创建 */}
      <DoubleSuspenseBoundary id={count} key={count} />
    </div>
  )
}

Попробуйте эффект:


Мы обнаружили, что каждый щелчокLoading..., состояние Pending пропало!Потому что каждый разcountувеличение,DoubleSuspenseBoundaryбудет воссоздан без запуска отложенной фиксации.

Основываясь на этом принципе, мы можем изменитьDoubleSuspenseBoundary, на этот раз мы даем только вложенныеSuspenseДобавьте ключи, чтобы они воссоздали неблокирующее состояние ожидания.

const DoubleSuspenseBoundary = ({ id }) => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <ComponentThatThrowPromise id={id} timeout={2000} />
      {/* ⚛️ 我们不希望这个 Suspense 阻塞 pending 状态, 给它加个key, 让它强制重新创建 */}
      <Suspense key={id} fallback={<div>Loading second...</div>}>
        <ComponentThatThrowPromise id={id + "second"} timeout={4000} />
      </Suspense>
    </Suspense>
  );
};

окончательный эффект

Это работа!



5️⃣ Можно ли использовать с Mobx и Redux?

Я тоже не знаю, проверьте:

mport React, { useTransition, useEffect } from "react";
import { createStore } from "redux";
import { Provider, useSelector, useDispatch } from "react-redux";
import SuspenseBoundary from "./SuspenseBoundary";
import Tick from "./Tick";

const initialState = { count: 0, tick: 0 };
const ADD_TICK = "ADD_TICK";
const ADD_COUNT = "ADD_COUNT";

const store = createStore((state = initialState, action) => {
  const copy = { ...state };
  if (action.type === ADD_TICK) {
    copy.tick++;
  } else {
    copy.count++;
  }
  return copy
});

export const Page = () => {
  const { count, tick } = useSelector(({ tick, count }) => ({ tick, count }));
  const dispatch = useDispatch();
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  const addTick = () => dispatch({ type: ADD_TICK });
  const addCount = () => dispatch({ type: ADD_COUNT });

  const handleClick = () => {
    addTick();
    startTransition(() => {
      console.log("Start transition with count: ", count);
      addCount();
      console.log("End transition");
    });
  };

  console.log(`App rendering with count(${count}) pendig(${pending})`);

  useEffect(() => {
    console.log("committed with", count, tick, pending);
  });

  return (
    <div className="App">
      <h1>Hello useTransition {tick}</h1>
      <div>
        <button onClick={handleClick}>ADD + 1</button>
        {pending && <span className="pending">pending</span>}
      </div>
      <Tick />
      <SuspenseBoundary id={count} />
    </div>
  );
};

export default () => {
  return (
    <Provider store={store}>
      <Page />
    </Provider>
  );
};


Давайте посмотрим на эффект операции:



В чем проблема? Весь интерфейсPending, весь интерфейс не простоAppЭто поддерево, и Тик тоже никуда не делся. Открываем консоль и видим предупреждение:

Warning: Page triggered a user-blocking update that suspended.

The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes.
Refer to the documentation for useTransition to learn how to implement this pattern.

Давайте посмотрим, как в настоящее время обновляются Hooks API Rudux и Mobx.По сути, все они используют механизм подписки для принудительного обновления после запуска события., основная структура выглядит следующим образом:

function useSomeOutsideStore() {
  // 获取外部 store
  const store = getOutsideStore()
  const [, forceUpdate] = useReducer(s => s + 1, 0)

  // ⚛️ 订阅外部数据源
  useEffect(() => {
    const disposer = store.subscribe(() => {
      // ⚛️ 强制更新
      forceUpdate()
    ))

    return disposer
  }, [store])

  // ...
}

То есть мыstartTransitionКогда состояние Redux обновляется в , событие принимается синхронно, а затем вызовforceUpdate.forceUpdateЭто состояние, которое фактически изменяется в контексте suspenseConfig..

Посмотрим еще раз на лог консоли:

Start transition with count 0
End transition
App rendering with count(1) pendig(true)  # 这里出问题了 🔴, 你可以和实验 3️⃣ 中的日志对比一下
App rendering with count(1) pendig(true)  # 实验 3️⃣ 中这里的 count 是 0,而这里的count是1,说明没有 defer!
read  1

Warning: App triggered a user-blocking update that suspended.
The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes.
Refer to the documentation for useTransition to learn how to implement this pattern.

Журнал может в основном определить проблему, счетчик не обновляется с задержкой, поэтому «синхронизация» запускает приостановку, что также является причиной предупреждения React. Поскольку useTransition все еще находится на экспериментальной стадии,Если приостановка не вызвана обновлением состояния в контексте startTransition , поведение по-прежнему не определено..

Но окончательное поведение немного метафизично, оно приведет к тому, что все приложение будет «ожидающим», и все обновления состояния не будут зафиксированы. Я тоже сильно запутался в этом, и сил нет вникать в это, остается только ждать последующее официальное обновление, а читатели тоже могут об этом подумать.

Следовательно, он временно не рекомендуется размещать состояния, которые запускают неизведенцию в Redux или MOBX.



Наконец, повторюсь,useTransitionвходитьPendingСтатус должен соответствовать следующим условиям:

  • Для обновлений лучше использовать собственный механизм состояния React, такой как Hooks или setState, в настоящее время не используйте Mobx и Redux.
  • Эти обновления вызывают приостановку.
  • обновление должно быть вstartTransitionэти обновления будут связаны сsuspenseConfig
  • Из повторных рендеров, вызванных этими обновлениями, по крайней мере один из них должен сработать.Suspense
  • этоSuspenseНе первое крепление


А как насчет использованияDeferedValue ?

Если вы понимаете вышеизложенное, тоuseDeferedValueЭто просто, это просто оболочка вокруг useTransition :

function useDeferredValue<T>(
  value: T,
  config: TimeoutConfig | void | null,
): T {
  const [prevValue, setValue] = useState(value);
  const [startTransition] = useTransition(config)

  // ⚛️ useDeferredValue 只不过是监听 value 的变化,
  // 然后在 startTransition 中更新它。从而实现延迟更新的效果
  useEffect(
    () => {
      startTransition(() => {
        setValue(value);
      })
    },
    [value, config],
  );

  return prevValue;
}

useDeferredValueПросто слушайте с useEffectvalueизменения, затем обновите их в startTransition . Чтобы добиться эффекта отложенного обновления. Как упоминалось в эксперименте 1️⃣ выше, React понизит приоритет обновлений в startTransition, что означает, что они будут отложены, когда транзакции заняты.



Суммировать

В начале мы представили сценарий использования useTransition и позволили странице реализоватьPending -> Skeleton -> CompleteПуть обновления пользователя может оставаться на текущей странице при переключении страниц, чтобы страница оставалась отзывчивой. Это намного удобнее, чем показывать бесполезную пустую страницу или состояние загрузки.

Конечно, при вышеприведенных предположениях загрузка данных происходит очень медленно.Если загрузка данных происходит очень быстро, с помощью механизма useTransition мы можем запретить пользователю видеть состояние загрузки, что позволяет избежать дрожания страницы и мерцания, которое выглядит так: нет процесса загрузки.

Затем мы кратко познакомили с принципом работы и условиями использования Transition. Если обновление состояния в startTransition вызывает приостановку, соответствующий компонент перейдет в состояние ожидания. В состоянии Pending изменения, заданные в startTransition, будут отложены для фиксации. Состояние ожидания будет продолжаться до тех пор, пока приостановка не будет готова или не истечет время ожидания.

useTransition необходимо использовать в сочетании с Suspense для выполнения магии. Существует также вариант использования, когда мы можем размещать обновления с низким приоритетом в startTransition. Например, если обновление стоит дорого, вы можете поместить его в startTransition.Эти обновления уступят приоритетным задачам, а React задержит или объединит более сложное обновление, чтобы страница оставалась отзывчивой.



Ок, введение режима Concurrent подходит к концу, это информация из первых рук на китайском языке. Написание этих статей занимает большую часть моего свободного времени, если вам нравятся мои статьи, пожалуйста, дайте мне больше лайков и отзывов.



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