«Новые функции React18» объясняют мастеру взаимодействия с пользователем на простом языке — переход

внешний интерфейс JavaScript React.js
«Новые функции React18» объясняют мастеру взаимодействия с пользователем на простом языке — переход

В React 18 был представлен новый API —startTransitionТакже есть два новых крючка -useTransitionиuseDeferredValue, они по существу неотделимы от понятияtransition.

Изучая эту главу, вы получите следующее:

  • TransitionПервоначальный замысел, какую проблему он решил.
  • startTransitionиспользование и принципы.
  • useTranstionиспользование и принципы.
  • useDeferredValueиспользование и принципы.

что вызваноtransitionАнглийский перевод'переход', то переход здесь относится к эффекту перехода данных, показывающих переход с нуля в обновлении. использоватьReactWgПредложение в startTransition описывает startTransition .

startTransition сохраняет отзывчивость страницы при обновлении большого экрана, этот API может помечать обновления React как особый тип обновления.transitions, с этим конкретным обновлением React смог сохранить визуальную обратную связь и отзывчивость браузера.

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

Две переходные миссии

1 Рождение перехода

Почему появляется Переход? Переход по сути решает проблему параллелизма рендеринга.В описании startTransition в React 18 много раз упоминалась ситуация «большого экрана».Под большим экраном здесь подразумевается не просто размер, а большой объем данных.Сценарии с много узлов элементов DOM, таких как визуализация данных на большом экране, в этом сценарии изменения, вызванные одним обновлением, могут быть огромными, поэтому частые обновления и частые вызовы для выполнения js-транзакций требуют от браузера выполнения большого объема работы по рендерингу. , поэтому пользователь чувствует себя застрявшим.

Переход в основном используется для некоторых менее срочных обновлений.До React 18 все задачи обновления считались срочными задачами.В React 18 он родилсяconcurrent ModeРежим, в этом режиме рендеринг может быть прерван, а задачи с низким приоритетом могут позволить задачам с высоким приоритетом сначала обновить рендеринг. Можно сказать, что React 18 способствует хорошему пользовательскому опыту. отconcurrent ModeприбытьsusponseсноваstartTransitionНесомненно, все это вращается вокруг лучшего пользовательского опыта.

startTransition зависит отconcurrent ModeРендеринг в параллельном режиме. То есть в React 18 используетсяstartTransition, то нужно сначала включить concurrent mode, то есть нужно пройтиcreateRootСоздать рут. Давайте сначала посмотрим на разницу между созданием Root в двух режимах.

традиционная унаследованная модель

import ReactDOM from 'react-dom'
/* 通过 ReactDOM.render  */
ReactDOM.render(
    <App />,
    document.getElementById('app')
)

v18 параллельный режим параллельный режим

import ReactDOM from 'react-dom'
/* 通过 createRoot 创建 root */
const root =  ReactDOM.createRoot(document.getElementById('app'))
/* 调用 root 的 render 方法 */
root.render(<App/>)

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

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

  • Первый: форма ввода должна получать статус в режиме реального времени, поэтому она контролируется, поэтому для обновления содержимого ввода необходимо запустить задачу обновления.
  • Второе: меняется входной контент, фильтрация списка, повторная отрисовка списка — тоже задача.

Для первого типа обновления при вводе ожидается, что визуальные изменения появятся немедленно.Если вводимый контент задерживается и отображается при вводе, это даст пользователю крайне плохое визуальное впечатление. Второй тип обновления заключается в фильтрации данных в списке и отображении списка в соответствии с содержанием данных.Этот тип обновления имеет более низкий приоритет, чем предыдущий. Затем, если пользователь предпочитает изменить состояние поля ввода во время процесса поиска ввода, то при нормальных обстоятельствах событие onChange привязывается к вводу, чтобы инициировать два вышеуказанных типа обновлений.

const handleChange=(e)=>{
   /* 改变搜索条件 */ 
   setInputValue(e.target.value)
   /* 改变搜索过滤后列表状态 */
   setSearchQuery(e.target.value)
}

написано выше, тоsetInputValueиsetSearchQueryРезультирующее обновление является обновлением того же приоритета. И сказал ранее,Приоритет обновления изменения состояния поля ввода выше, чем приоритет обновления списка., в это время на сцену выходит наш главный герой. использоватьstartTransitionРазличайте два обновления.

const handleChange=()=>{
    /* 高优先级任务 —— 改变搜索条件 */
    setInputValue(e.target.value)
    /* 低优先级任务 —— 改变搜索过滤后列表状态  */
    startTransition(()=>{
        setSearchQuery(e.target.value)
    })
}
  • Как и выше, менее срочная задача обновления setSearchQuery изолируется через startTransition. Как это работает в реальной ситуации? Давайте проверим это.

2 сценария моделирования

Далее мы моделируем описанный выше сценарий. Процесс примерно такой:

  • Есть поле поиска и список из 10 000 элементов данных, каждый из которых имеет одну и ту же копию.
  • Изменение ввода изменяет содержимое ввода в реальном времени (первое обновление), а затем выделяет то же значение поиска в списке (второе обновление).
  • Управление одной кнопкой Нормальный режим |transitionмодель.
/*  模拟数据  */
const mockDataArray = new Array(10000).fill(1)
/* 高量显示内容 */
function ShowText({ query }){
   const text = 'asdfghjk'
   let children
   if(text.indexOf(query) > 0 ){
       /* 找到匹配的关键词 */
       const arr = text.split(query)
       children = <div>{arr[0]}<span style={{ color:'pink' }} >{query}</span>{arr[1]} </div>
   }else{
      children = <div>{text}</div>
   }
   return <div>{children}</div>
}
/* 列表数据 */
function List ({ query }){
    console.log('List渲染')
    return <div>
        {
           mockDataArray.map((item,index)=><div key={index} >
              <ShowText query={query} />
           </div>)
        }
    </div>
}
/* memo 做优化处理  */
const NewList = memo(List)
  • ListКомпонент рендерит 10 000ShowTextкомпоненты. В компоненте ShowText динамическое выделение будет осуществляться посредством входящего запроса.
  • потому что каждое изменениеqueryВыполнит 10000 повторных обновлений рендеринга, а также отобразит выделенное содержимое запроса, поэтому удовлетворитеПараллельный рендерингместо действия.

Следующим шагом является написание компонента приложения.

export default function App(){
    const [ value ,setInputValue ] = React.useState('')
    const [ isTransition , setTransion ] = React.useState(false)
    const [ query ,setSearchQuery  ] = React.useState('')
    const handleChange = (e) => {
        /* 高优先级任务 —— 改变搜索条件 */
        setInputValue(e.target.value)
        if(isTransition){ /* transition 模式 */
            React.startTransition(()=>{
                /* 低优先级任务 —— 改变搜索过滤后列表状态  */
                setSearchQuery(e.target.value)
            })
        }else{ /* 不加优化,传统模式 */
            setSearchQuery(e.target.value)
        }
    }
    return <div>
        <button onClick={()=>setTransion(!isTransition)} >{isTransition ? 'transition' : 'normal'} </button>
        <input onChange={handleChange}
            placeholder="输入搜索内容"
            value={value}
        />
       <NewList  query={query} />
    </div>
}

Давайте посмотрим, что делает приложение.

  • Сначала обработайте событие onchange через событие handleChange.
  • buttonкнопка для переключенияtransition(установить приоритет) иnormal(нормальный режим). Далее стоит стать свидетелем волшебного момента.

Эффекты в обычном режиме:

1.gif

  • Хорошо видно, что в обычном режиме вводимый контент и представление контента ненормально застревают, что создает очень плохое взаимодействие с пользователем.

Эффект в переходном режиме:

2.gif

  • После обработки большого количества одновременных задач через startTransition хорошо видно, что ввод будет отрисовываться нормально, а задача обновления списка будет отставать, но пользовательский опыт значительно улучшился.

общий эффект:

3.gif

  • Приходите испытать немного магии startTransition.

Суммировать:Из вышеизложенного мы можем интуитивно увидеть, что startTransition играет ключевую роль в обработке задач перехода и оптимизации взаимодействия с пользователем.

3 Почему бы не установить время ожидания

Вышеупомянутая проблема может бытьsetSearchQueryОбновление упаковано вsetTimeoutВнутри вроде следующее.

const handleChange=()=>{
    /* 高优先级任务 —— 改变搜索条件 */
    setInputValue(e.target.value)
    /* 把 setSearchQuery 通过延时器包裹  */
    setTimeout(()=>{
        setSearchQuery(e.target.value)
    },0)
}
  • Здесь через setTimeout обновление ставится внутрь setTimeout, тогда мы все знаем, что setTimeout — это задача-задержка, она не будет блокировать нормальную отрисовку браузера, и браузер установит setTimeout в следующий простой. Итак, как это работает? Давайте взглянем:

4.gif

  • Как вы можете видеть выше, setTimeout действительно может улучшить состояние ввода, но поскольку сам setTimeout также является задачей макроса, а каждый триггер onchange также является задачей макроса, setTimeout также повлияет на интерактивность страницы.

Подводя итог, преимущества и отличия startTransition по сравнению с setTimeout:

  • С одной стороны: логика обработки startTransition и setTimeout имееточень важное отличие, setTimeout — это асинхронно отложенное выполнение, а функция обратного вызова startTransition выполняется синхронно. Любое обновление в startTransition будет помеченоtransition, React будет оценивать этот флаг при обновлении, чтобы решить, следует ли завершить обновление. Таким образом, Transition можно понимать как обновление раньше, чем setTimeout. Но при этом необходимо обеспечить нормальный отклик UI.На устройстве с хорошей производительностью задержка между двумя обновлениями перехода будет небольшой, а на медленном устройстве задержка будет большая, но это не повлияет на реакцию пользовательского интерфейса.

  • С другой стороны, только в приведенном выше примере вы можете видеть, что для параллельных сценариев рендеринга setTimeout по-прежнему будет замораживать страницу. Потому что по истечении таймаута будут выполняться и задачи setTimeout, а они тоже макрозадачи, взаимодействующие с пользователем, поэтому взаимодействие со страницей все равно будет заблокировано. ТакtransitionДругое дело, в режиме conCurrent,startTransitionМожно прервать рендеринг, поэтому это не приведет к зависанию страницы.React позволяет выполнять эти задачи во время простоя браузера, поэтому при вводе вышеуказанного входного содержимого startTransition отдает приоритет обновлению входного значения. , а затем рендеринг списка .

4 Почему бы не дросселировать и не трясти

Тогда давайте подумаем над другим вопросом, а почему нет троттлинга и антишейка. Во-первых, могут ли троттлинг и антишейк решить проблему заикания? Ответ да, до этого нет апи типа перехода, можно только пройтиСтабилизаторидросселированиеБороться с этим, а потом бороться с антишейком.

const SetSearchQueryDebounce = useMemo(()=> debounce((value)=> setSearchQuery(value),1000)  ,[])
const handleChange = (e) => {
    setInputValue(e.target.value)
    /* 通过防抖处理后的 setSearchQuery 函数。  */
    SetSearchQueryDebounce(e.target.value)
}
  • SetSearchQuery будет обрабатывать анти-дрожание, как указано выше. Потом смотрим на эффект.

5.gif

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

  • С одной стороны, стабилизация образа дроссельной заслонки также по существу, по существу, устанавливается, но контролирует частоту выполнения, то содержимое можно найти через печать, принцип - сделать рендеринг раз. Переходы и сравнивали его, не уменьшали количество рендеринга.

  • С другой стороны, троттлинг и анти-тряска должны быть эффективно освоены.Delay TimeВремя задержки. Если время слишком велико, это даст людям ощущение задержки рендеринга. Если время слишком мало, это вызовет предыдущую проблему, аналогичную setTimeout(fn,0). И startTransition не нужно так много учитывать.

5 Влияние на производительность компьютера

Эффект перехода более заметен на медленных компьютерах, давайте посмотримReal world example

Обратите внимание на скорость слайдера

  • Производительность обработки на устройствах выше и быстрее. Не используйте startTransition .

12.gif

  • Производительность обработки на устройствах выше и быстрее. Используйте начальный переход.

13.gif

  • На медленных устройствах с низкой производительностью обработки startTransition не используется.

14.gif

  • На медленных устройствах с низкой производительностью обработки используйте startTransition.

15.gif

Три функции перехода

Теперь, когда было сказано об изначальном намерении перехода, давайте посмотрим на введение функции перехода.

1 Что такое перегрузка.

Обновление состояния, как правило, будет разделено на две категории:

  • Обновление первого срочного задания. Например, некоторые взаимодействия с пользователем, клавиши, клики, набор текста и так далее.
  • Вторая категория — задача обновления перехода. Например, переходы пользовательского интерфейса из одного представления в другое.

2 Что такое startTransition

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

startTransition(scope)
  • scope — это callback-функция, и задачи обновления в ней будут помечены какЗадача обновления перехода, задача обновления перехода будет понижена до приоритета обновления в параллельной сцене рендеринга, и обновление будет прервано.

использовать

startTransition(()=>{
   /* 更新任务 */
   setSearchQuery(value)
})

3 Что такое useTranstion

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

Для решения этой проблемы React предоставляет хук с состоянием isPending — useTransition. Выполнение useTransition возвращает массив. Массив имеет два значения состояния:

  • Первый — это флаг при переходе в состояние — isPending.
  • Второй — это метод, который можно понимать как описанный выше startTransition. Задачу обновления внутри можно превратить в задачу перехода.
import { useTransition } from 'react' 

/* 使用 */
const  [ isPending , startTransition ] = useTransition ()

Затем, когда задача находится в состоянии наведения,isPendingзаtrue, который может отображаться как пользовательский интерфейс, которого ждет пользователь. Например:

{ isPending  &&  < Spinner  / > }

useTranstion на практике

Далее, давайте попрактикуемся с useTranstion или повторно используем приведенную выше демонстрацию. Модификация приведенного выше демо.

export default function App(){
    const [ value ,setInputValue ] = React.useState('')
    const [ query ,setSearchQuery  ] = React.useState('')
    const [ isPending , startTransition ] = React.useTransition()
    const handleChange = (e) => {
        setInputValue(e.target.value)
        startTransition(()=>{
            setSearchQuery(e.target.value)
        })
    }
    return  <div>
    {isPending && <span>isTransiton</span>}
    <input onChange={handleChange}
        placeholder="输入搜索内容"
        value={value}
    />
   <NewList  query={query} />
</div>
}
  • как указано вышеuseTransition,isPendingПредставляет переходное состояние, когда в переходном состоянии отображаетсяisTransitonнамекать.

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

6.gif

Видно, что состояние во время перехода фиксируется точно.

4 Что такое useDeferredValue

В приведенном выше сценарии мы обнаружили, что запрос по сути также является значением, но обновление запроса отстает от обновления значения. Что ж, React 18 обеспечиваетuseDeferredValueМожно сделать вывод запаздывания состояния. Эффект реализации useDeferredValue также похож наtranstion, при выполнении срочной задачи получается новое состояние, и это новое состояние называется DeferredValue.

Каковы сходства и различия между useDeferredValue и useTransition выше?

Тот же пункт:

  • useDeferredValue по существу совпадает с внутренней реализацией, а useTransition помечен как задача обновления перехода.

разница:

  • useTransition превращает задачу обновления внутри startTransition в задачу переходаtranstion, а useDeferredValue — получить новое значение из исходного значения через задачу перехода, и это значение используется в качестве состояния задержки.Один — обрабатывать часть логики, другой — создавать новое состояние.
  • Еще одно отличие useDeferredValue в том, что эта задача по сути выполняется внутри useEffect, а внутренняя логика useEffect выполняется асинхронно, поэтому она в определенной степени отстаетuseTransition.useDeferredValue = useEffect + transtion

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

export default function App(){
    const [ value ,setInputValue ] = React.useState('')
    const query = React.useDeferredValue(value)
    const handleChange = (e) => {
        setInputValue(e.target.value)
    }
    return  <div>
     <button>useDeferredValue</button>
    <input onChange={handleChange}
        placeholder="输入搜索内容"
        value={value}
    />
   <NewList  query={query} />
   </div>
}
  • Как вы можете видеть выше, запрос — это значение, сгенерированное useDeferredValue.

Эффект:

7.gif

Четыре принципа

Далее идет принципиальная ссылка, от startTransition к useTranstion и к useDeferredValue. Принцип очень прост по своей природе.

1 startTransition

Давайте сначала посмотрим, как реализован самый простой startTransition.

react/src/ReactStartTransition.js -> startTransition

export function startTransition(scope) {
  const prevTransition = ReactCurrentBatchConfig.transition;
  /* 通过设置状态 */
  ReactCurrentBatchConfig.transition = 1;
  try {  
      /* 执行更新 */
    scope();
  } finally {
    /* 恢复状态 */  
    ReactCurrentBatchConfig.transition = prevTransition;
  }
}
  • startTransitionПринцип очень прост, немного похож на логику пакетной обработки batchUpdate в React v17. Это путем установки переключателя, и переключательtransition = 1, а затем выполнить обновление, задачи обновления в нем получатtranstionлоготип.

  • Далее он будет обработан отдельно в одновременном режимеtranstionтип обновления.

Его принципиальная схема показана ниже.

9.jpg

2 useTranstion

следующий взглядuseTranstionвнутренняя реализация.

react-reconciler/src/ReactFiberHooks.new.js -> useTranstion

function mountTransition(){
    const [isPending, setPending] = mountState(false);
    const start = (callback)=>{
        setPending(true);
        const prevTransition = ReactCurrentBatchConfig.transition;
        ReactCurrentBatchConfig.transition = 1;
        try {
            setPending(false);
            callback();
        } finally {
            ReactCurrentBatchConfig.transition = prevTransition;
        }
    }
     return [isPending, start];
}

Этот код не является исходным кодом. Я комбинирую и сжимаю содержимое в исходном коде.

  • Как видно из вышеизложенного, useTranstion, по сути,useState + startTransition.
  • Изменение статуса PENDING через USSTATE. Во время выполнения MountTransition триггер дваждыsetPending, однажды вtransition = 1до, один раз после. будет обновляться нормально один разsetPending(true), когда-то будет действовать какtransitionОбновление переходной миссииsetPending(false);, поэтому время перехода можно точно зафиксировать.

Его принципиальная схема показана ниже.

10.jpg

3 useDeferredValue

Наконец, давайте посмотрим наuseDeferredValueПринцип внутренней реализации.

react-reconciler/src/ReactFiberHooks.new.js -> useTranstion

function updateDeferredValue(value){
  const [prevValue, setValue] = updateState(value);
  updateEffect(() => {
    const prevTransition = ReactCurrentBatchConfig.transition;
    ReactCurrentBatchConfig.transition = 1;
    try {
      setValue(value);
    } finally {
      ReactCurrentBatchConfig.transition = prevTransition;
    }
  }, [value]);
  return prevValue;
}

Поток обработки useDeferredValue выглядит следующим образом.

  • Как вы можете видеть из приведенного выше, useDeferredValue по существуuseDeferredValue = useState + useEffect + transition
  • Передавая значение useDeferredValue, useDeferredValue сохраняет состояние через useState.
  • Затем в useEffect проходитtransitionРежим обновления значения. Это гарантирует, что DefredValue отстает от обновлений государства и удовлетворяетtransitionПринцип обновления перехода.

Его принципиальная схема показана ниже.

11.jpg

Четыре резюме

Знания, рассматриваемые в этой главе, следующие:

  • TransitionПервоначальный замысел, какую проблему он решил.
  • startTransitionиспользование и принципы.
  • useTranstionиспользование и принципы.
  • useDeferredValueиспользование и принципы.

Справочная документация