Головна / Статті / Де зберігається значення useState: елементи React проти волокон.

Де зберігається значення useState: елементи React проти волокон.

Чому функційний компонент React забуває все між викликами, чому елементи не можуть зберігати стан, та як поле memoizedState волокна зберігає значення useState живими.

1724 слів

Компонент-функція — це просто функція, і функції забувають свої локальні змінні в момент повернення. Проте useState повертає оновлене значення під час кожного відображення, ніби функція його пам’ятає. Цей матеріал точно відповідає на одне конкретне запитання: де фізично знаходиться це значення між відображеннями? До кінця ви зможете розрізняти два об’єкти, які React створює для кожного компонента — елемент та фібер, і пояснити, який з них містить стан та чому.

Загадка: функція без пам’яті

Почнемо з найзнайомішого компонента — лічильника:

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

Якщо натиснути один раз, відобразиться 1, якщо натиснути знову — 2. У цьому немає нічого дивного, поки ви не подивитеся на це як на звичайний JavaScript. Counter — це функція. Кожного разу, коли функція викликається, її локальні змінні створюються з нуля, а коли вона повертається, вони зникають. Наступний виклик починається заново.

Згідно з цією логікою, коли React вдруге викликає Counter(), рядок useState(0) мав би знову повернути 0. Однак він повертає 1. Щось поза межами функції мусить зберігати це число від одного виклику до іншого. Це не може бути тіло функції, адже саме так не поводяться функції. То що ж?

Виключення елемента

Є очевидний кандидат, який виявляється неправильним, і його виключення робить справжню відповідь зрозумілішою.

Рядок <button onClick={...}>{count}</button> є JSX. Браузери ніколи не виконують JSX; компілятор, такий як Babel або компілятор TypeScript, переписує його на звичайний виклик функції перед розгортанням коду. Концептуально результат є наступним:

React.createElement("button", { onClick: fn }, count)

Завдяки автоматичному рунтайму JSX, введеному у React 17, компільований виклик насправді є jsx() з модуля react/jsx-runtime, а не React.createElement, проте обидва виконують однакову функцію. Якою б не була назва функції, вона повертає звичайний об’єкт:

{
  type: "button",
  props: { onClick: fn, children: 1 },
}

У React це називається елементом. Це опис, а не жива структура: невеликий запис про те, що тут має з’явитися кнопка з певним обробником кліку та відображати це число. Справжні елементи містять ще кілька полів, таких як key, ref та внутрішній маркер $typeof, але жодне з них не зберігає історію.

Тепер ключове спостереження. Кожен виклик Counter створює абсолютно новий елемент. Якщо натиснути кнопку, виконується Counter(), повертається новий об’єкт, а попередній стає непотрібним. Якби стан зберігався в елементі, не було б способу перейти від відображення двох елементів до відображення одного, оскільки об’єкт для відображення одного більше не існує, коли починається відображення двох.

Ця тимчасовість є навмисною. Елементи є дешевими саме тому, що вони створюються з нуля при кожному оновленні та їм ніколи не потрібно підтримувати синхронність з чимось іншим. Це також означає, що вони не можуть знаходитися там, де зберігається count.

Окрім елементів, React зберігає ще один об’єкт довшого терміну існування для кожного компонента, який було розміщено на екрані. Його не створюють заново при кожному оновленні. Він створюється, коли компонент вперше розміщується, а потім зберігається та оновлюється протягом усього часу, поки компонент залишається на екрані. Це і є fiber.

Волокна часто описують як щось таємниче, але на найнижчому рівні волокно — це звичайний об’єкт JavaScript. За ним не стоїть жодна спеціальна конструкція під час виконання. Якщо зупинитися у дебагері всередині механізму узгодження React та переглянути одне волокно, ви побачите звичайний об’єкт із набором властивостей, який можна створити самостійно за допомогою пари фігурних дужок. Їх також можна переглянути у браузері: React прив’язує внутрішні ключі до елементів DOM, які вказують на їхні волокна, і саме так React DevTools їх знаходить.

Замість того, щоб перераховувати всі поля одночасно, корисніше створювати волокно для Counter по одній властивості за раз. Відразу після монтування мінімальна версія виглядає так:

{
  type: Counter,
}

type: чия це облікова система

type — це найпростіше поле. Воно визначає, який компонент відстежує ця волокно. type: Counter означає, що об’єкт створений для керування інстанцією Counter, яка містить посилання на саму функцію.

Елементи-хости також мають волокна. Кнопка, яку відображає Counter, має власне волокно, а її type — це рядок "button", а не функція. Тож волокно <Counter /> має значення { type: Counter }, а волокно <button> — { type: "button" }: одне й те саме поле, одна й та сама мета, але воно вказує або на ваш компонент, або на вбудовану тег-структуру.

type також відіграє роль у примиренні. Коли React порівнює новий елемент із існуючим фібром у тому самому положенні, збіг type дозволяє йому повторно використати цей фібр та його стан, тоді як різний type змушує викинути старий фібр та створити новий. Саме тому зміна між двома різними компонентами в одному місці скидає їхній стан.

type нічого не говорить про те, як зберігається стан. Для цього потрібне наступне поле.

memoizedState: де насправді знаходиться підрахунок

useState потребує місця поза функцією для зберігання поточного значення, оскільки локальні змінні функції скидаються при кожному виклику. Цим місцем є поле у фібрі під назвою memoizedState. „Memoized“ просто означає запам’ятоване: збережене з попереднього разу замість перерахунку.

Додавання цього до скетчу дає:

{
  type: Counter,
  memoizedState: { count: 0 },
}

Це спрощена ілюстрація. У реальній реалізації memoizedState у фібрі функційного компонента вказує на перший елемент ланцюгового списку об’єктів hook — по одному на кожен виклик hook, а число 0 знаходиться у власному полі memoizedState цього hook, а не в об’єкті { count: 0 }. React не знає, що ваша змінна називається count; він знає лише „значення першого hook“. Для розуміння того, де знаходиться стан, достатньо спрощеної версії.

Тепер слідкуйте за кліком. React знову викликає Counter(). Коли виконання доходить до useState(0), хук не повертає значення 0 у вашому коді. Він шукає збережений стан фібри, знаходить поточне значення (яке дорівнює 1 після обробки кліку) та повертає його. Аргумент для useState — це лише початкове значення: його використовують під час першої відрендеризації, а потім ігнорують. Відтоді джерелом істини є фібра, а не літеральне значення у тілі функції.

Це повна відповідь на початкову загадку. Кількість не знаходиться у функції, яка забуває все між викликами, і не у елементі, який видаляється після кожної відрендеризації. Вона зберігається у фібрі — окремому об’єкті, який React зберігає та оновлює під час кожної відрендеризації Counter.

Дві практичні наслідки

Ця модель пояснює кілька явищ, які інакше здаються довільними:

  • Зміна аргументу useState після монтування не впливає на збережене значення, оскільки React читає його лише вперше. Якщо потрібно скинути стан з пропсів, змініть key компонента, щоб React створив нову фібру.
  • Оскільки фібра знаходиться за позицією в дереві, стан належить до того місця, де рендерується компонент, а не до визначення функції. Два елементи <Counter /> поруч отримують дві окремі фібри та два незалежні лічильники.

Що ще містить фібра окрім цих двох полів

type та memoizedState достатні для вирішення проблеми стану, але справжній фібер містить набагато більше інформації. Він зберігає посилання на вузол DOM, який був створений, вказівники на його батька, першу дитину та наступного брата-вузол, щоб React міг пройтися по дереву, а також попередні значення props для порівняння з новими. Саме ці поля визначають, як React визначає зміни та як він просувається по всьому дереву компонентів, що є окремою проблемою від того, де знаходиться окреме значення.

Зараз можна чітко визначити межу між цими двома об’єктами, адже їх змішування є причиною багатьох плутанин щодо відображення:

Element                           Fiber
--------                          -----
Created by React.createElement    Created internally by React
New object every render           Same object, updated in place
Discarded right after             Persists for the component's
  React reads it                    entire mounted lifetime
Holds no history                  Holds memoizedState, the real
                                     remembered value across renders
Describes what should exist       Is the thing that actually exists,
                                     with real memory attached to it

Компактний спосіб запам’ятати це: елемент — це запит, який ви надсилаєте до React, тоді як фібра — це власний запис React про те, що наразі існує. Кожне відображення створює нову партію дешевих, безпам’ятних елементів та передає їх дереву фібр, яке вже існувало з попереднього відображення та зберігає все, що має залишитися. Одна сторона описує, інша — пам’ятає.

Одна незрозуміла деталь: фібри існують парами

Твердження, що фібра „оновлюється на місці“, є корисною первинною оцінкою, але це не вся правда. Насправді React зберігає дві версії кожної фібри — одну, яка відповідає тому, що зараз відображається на екрані, та іншу, яка готується до наступного оновлення, і змінює їх місцями. Саме така схема дозволяє React працювати над оновленням, не порушуючи видимий інтерфейс користувача, і це потребує окремого пояснення.

Основні висновки

  • Локальні змінні функційного компонента створюються заново при кожному виклику, тому сама функція не може зберігати стан.
  • JSX компілюється у виклики, які створюють елементи: прості, одноразові описи, які перебудовуються при кожному відображенні.
  • Fibers — це звичайні об’єкти, які існують під час монтування компонента; type вказує, який компонент відстежує fiber.
  • useState читає та записує значення через memoizedState fiber, який насправді вказує на список об’єктів hook у порядку їх виклику.
  • Початкове значення, передане до useState, має значення лише під час монтування; після цього джерелом істини є fiber.

Пов’язана література