Чаму існуюць правілы гачкоў: волокна, спісы гачкоў і дыспэтчэры
Экскурсія па внутраннях React: вузлы Fiber, двойная буферазацыя, каналы, спіс звязкаў hook і тое, як кожная вбудованая група hook храніць свой стан і плануе виконанне задач.
Большая частка разработчыкаў на React можа запамятаць Правілы хуков, але значна меншая колькасць можа поясніць, чаму ў разе іх наражання стан дастаёцца зіпсаваным, а не проста выдаецца корисная памятка пра аднойшчыну. Адказ крыўціцца ў внутрошняй структуре React: кожны вызов хука стае вузлом у лінейным списку, які зберагаецца на Fiber, і React вылучае кожны вузел абсалютна на аднойшчыну з порядку, у яком вызываюцься хукі. Шырокі практычны адказ раз’яснюе, як функцыонуе Fiber, об’ект хуків, механізмы dispatchers, якія React выкарыстоўвае пад час рендараў, а таксама внутрошняе функцыонаванне кожной групы хуков, тады правілы перестаюць здавацца довільнымі і становяцца нарадзею паслядкавае дызайну.
Вам не патрэбныя гэтыя знанні, каб напісаць форму чытаў дадзеныя. Але яны станавяцца корыстнымі, калі трэба дыбагаваць застарелыя значэнні, выбіраць между useEffect і useLayoutEffect, чыста калі цікавіцца, чаму дзеціны элементы, якія зберагаюцца ў памяці, адноўляюцься зноў.
Короткае падсумаванне: што заменілі хукі і якія правілы ўжо ўсталяваліся
Hooks был адаптаван у React 16.8, і колькая ўбудованых хукоў вырасла да адносна сімнадцаты. Яны рашылі тры давялічныя проблемы компонентаў класаў: логіку, залежную ад стану, было важка перысвайць між рознымі компонентамі, а супаўзяную логіку распадзялі між методамі жыццёвага циклу, чыяму компоненты ставалі занадта вялікімі, а самыя класы JavaScript (звязыванне this, розумеўце жыццёвых циклу) плуталі багато разработчыкаў.
Разам з хукамі з’явілася сукупнасць правілаў:
- Выклікайце хукі на верхнім рэвэлі тэла функцыйныя компонента.
- Выклікайце хукі на верхнім рэвэлі тэла спецыяльнага хука.
- Ніколі не выклікайце хукі ўнутрь умоваў або цыклаў.
- Ніколі не выклікайце хукі пасля умовнага раннега
return. - Ніколі не выклікайце хукі ўнутрь обрабоўчыкаў змян.
- Ніколі не выклікайце хукі ў компонентах класаў.
useEffect, useMemo або useReducer.try, catch або finally.Паляганне на гэтых правілах прыводзіць да аўтазаавярэнняў, памылак або, што ўжо горш, до тонкіх багоў. Короткая адпаведзь — хукі компонентата утвараюць односвязаны список, які прыўяжаны да вузла Fiber, яным ёсць об’ект JavaScript з станам, які React зберагае для кожнага компонента. Ёсць прычыны, чаму гэта важна: пачніце з самага Fiber. Якщо вы вже добра яго знаеце, перайдзіце да раздзела пра об’ект хука. Чырвоныя введчыкі ў роботе з прызначэнням стану можна пазнакоміцца ў статыце «Стварэнне ментальнага моделю для прызначэння стану і хукоў у React».
React Fiber: двігун, у яком жывуць хукі
Fiber — это механізм супраавтарызацыі у React, які быў адкрыты ў React 16 як повна перапісва спосабу, яким React вычыляе та прыменяе апдэты інтэрфейсу.
Проблема старог супраавтарызатора
До появы Fiber React выкарыстоўваў тое, што часта называюць супраавтарызаторам стаку. Пры кожным апдэце ён рекурсыўна праходзіл па дрэве компонентаў, а рекурсыўны праход па стаку вызваў JavaScript не можа быць зупінены ў паўпацёку: як толькі ён запускаецца, ён працюе да таго часу, пакі не будзе обробленаўца ўсё дрэва. У большым дрэве, якое займала ўсю галоўную вяліну, анімацыі замарзалі, натысканне клавіш адставала, а інтэрфейс працаваў з перашкодамі. Што ўжо горш, не было можлівасці дазволіць трыбутнаму апдэту, такаму як натысканне клавішы, працаваць раней за вялікі, менш важны процес атрыбутавання, які ўжо штоў.
Шта дазволяе Fiber
Файбер — это просты ўжоект JavaScript, які представляе адну елементарную еўранцю роботы, прычысленую да экзанпляра компонента або вузла DOM. Паколькі React сам стежыць за гэтымі еўранцямі, а не павязваецца на стак дзванакоў, ён атрымлеў тры перавагі:
- Пазначыць на паузу і продажыць роботу. React можа зупініцца ў середзіне обрабоцы, дазволіць браузеру адмаўляцца іншым, болей срочным задачам, такім как ввод ад корыстніка, і пракратыць роботу з таго ж месца пазней.
- Надаць прыорітэт. Срочныя апдэйты могу перакрыць менш важлівыя.
- Павторна выкарыстоўваць або знікнуць. Якщо корыстнік перайдзе на іншую сторону, паколькі ўжо трывае адрасаванне, незакончаную роботу можна проста знікнуць.
Форма вузла Fiber
Кожны элемент React — будзь то компонент, вузел DOM або тэкстовы вузел — мае адпаведны файбер. Цэў вялікі ўжоект, які храніць параметры компонента, його стан і супакой да яго представлення ў DOM.
У змену на зберагачванне дзецэў у масавых структурах, фібры ствараюць дрэва за дапамою трохоў паказывачаў:
childвядзе да першага дзецячага элемента фібры.siblingвядзе да наступнай фібры на тым жа роўні.returnвядзе назад да родніка.
Обробка апдэйта значыць следаванне за гэтымі звязкамі: спускацца па child на максымальную глыбіну, пераходзіць у бок па sibling, і падымацца назад па return, калі завершыўся адпаведны галузь. Паколькі гэта звычайны цикл па паказывачах, а не рэкурсія, React можа зупніцца межы двума фібрамі.
Двойная буферазацыя з двума дрэвамі
Фібры запозначаюцься двойной буферазацыяй з програмавання графікі. У будзь-ям моменте React зберагае два дрэвы фібры ў памяці:
- Тэкучы дрэво точна падражае тое, што апісваецца на экране. React не зменяе яго пад час вычыслення апдэйта.
- Дрэво з работамі (WIP) ствараецца на фоне, калі ўтворяюцься змены. React клонуе тэкучыя фібры, якія трэба апдэйтаваць, і стварае новую версію празаўседы ўжо існуючай.
Калі дрэво з работамі готава, React зменяе паказчык корана. Дрэво з работамі стае тэкучым, і экран апісвае новы стан. Кожная фібра зберагае паказчык alternate да свайго адпаведніка ў іншым дрэве, і самэй гэтым стан хукіў перадаецца з адной рэндарованай версіі на іншую.
Фаза рэндараўання і фаза зафіксавання
Гэтая архітектура дзеліць кожны апдэйт на две фазы.
Этап адрабатарэння можа быць перарваны. React праходзіць па дрэва, вызывае функцыі вашага компонента, запускае хукі і прабоюе рэзультат з ныявным дрэвам, адночасна ствараючы дрэва WIP у памяці. Паколькі React керуе цыклам праходжэння, яго планавальнік можа перадаць керуванне браузэру кожныя калькі мілісекунд. Якщо корыстнік пачынае вводзіць тады, калі адрабатарэнне низкага прыоритету ўжо ідзе, React можа націснуць паузу, обработаць ввод і продаваць адрабатарэнне. Ён таксама можа збыць цалое дрэва WIP, калі новейшая, более срочная апдэйт зробіць яго застарелым. Паколькі адрабатарэнне можа выконвацца калькі разоў або ўжо ніколі не завершыцца, функцыі компонента павінны быць чыстымі: без побачных эфектаў падчас адрабатарэння.
Этап адаптавання ўсунення змян яе сінхронны. Калі дрэва WIP завершаецца, React адразу прыкладзея вырахаваныя змяны да рэальнага DOM. Што гэты крок не можа быць зупінены, таму што перыявленне падчас мутацый DOM прыведзе да таго, што корыстнік пабачыць часткова апдэйтаваны, некансістэнтны інтерфейс. Тут выконваюцца эфекты лэйауту, звярху плануецца выконанне пасіўных эфектаў, а таксама прыкладзяюцца рэфы.
Лейны: як React выбірае, што перыявляць
Ёнкі з’ясаваць, што можа перыявіць шта, React пазначае кожную апдэйтаванне лейнам. Лейны представленыя як біты ў масце бітов, што дазволяе дышча прыкладзяць і параўнаць прыорітэты. У загальных рысах:
- Сінхронны лейн для дыскрэтных, трагічных інтеракцый, такіх як клікі і нажатыя клавішы (паўтаральныя з’явы, такія як хоўварынг аб скарыранне, маюць свой лейн з высокім прыорітэтом).
- Лейны пераходу для роботы, якая можа быць перыявлена: апдэйтаванні на фоне, перызначэнне відображэння на аднойчына па данных, змена вікон.
Вы ніколі не працюеце безпасяродна з Fiber, але ён являецца адной з ключоўых функцыяй React 18 і 19. Адночасная апрацоўка, Suspense, useTransition і useDeferredValue — усе гэта залежыць ад можлівасці перарыву апрацоўкі.
Об’ект хука і чаму порядак вызываў ёсць важлівым
У функцыйных компонентах няма экземпляра для зберагчання стану, таму React зберагае яго на файбары ў поле пад назвай memoizedState. Для функцыйных компонентаў гэтае поле вядуць да першага хука ў односвязаным списку.
Кожны вызыв хука падчас апрацоўкі супарадніцься з об’ектам, які прыблізна выглядае так:
{
memoizedState: any, // The internal state of the hook
baseState: any, // The state before any unprocessed updates
baseQueue: Update | null, // Updates that were skipped due to priority
queue: UpdateQueue | null, // Circular linked list of pending state updates
next: Hook | null // Pointer to the next hook in the component
}
memoizedState хука зберагае яго значэнне (стан для useState, запис прыема дзеяння для useEffect, запам’ятаваны пара для useMemo), queue зберагае чакаючыяся апдэйты, baseState і baseQueue фіксуюць апдэйты, якія былі праўлены, таму што ўсё, што знаходзілася ў іх канцэртнай лініи, не обраблялася, а next выканае перыявленне да наступнага хука.
Зверніце ўвагу на тое, чаго не хапае: няма ключа або імені. Пад перыядзеўленнем React проста праходзіць па спісе з початку, паруячы першы вызов хука з першым вузлом, другі — з другім і так далей. Якщо хук вызываецца ўнутры if і умова зменшыцца, кожны наступны вызов будзе паруваны з неправым вузлом, і стан аднаго хука прасочыцца ў іншы. Гэта яе основная прычына — правілы хуков: яны гарантуюць, што тыя ж самыя хуки запускаюцца ў той жа порядак пад кожным перыядзеўленнем. Правілы пра ціклы, раннія вяртання, блокі try і калебэксы — усе гэта ў варіяцыях таго ж трэбавання.
Адна сучасная асаблівасць падтверджае гэты прынцып: API use у React 19 можа быць вызываны за умовам, самэўсёлкі таму, што ён не павязаны з вузламі спіса такім жа спосабом.
Дыспэтчэры: тое ж імя хука, разныя реалізацыі
useState, які вы імпортуеце, ў сутнасці являюцца простым кэшэрам. У часе выканання ён перадае запыт таму дыспэтчэру, які React установіў для данага этапу. У старыях версіях React гэта робіцца через ReactCurrentDispatcher; у новыях версіях дыспэтчэр знаходзіцца ў внутранім спяльным об’екте React, але ідея застаецца тая ж.
- HooksDispatcherOnMount актыўны пад час першага атрыбутавання.
useStateсупрацоўвае зmountState, які стварае новы об’ект хука, задае йому пачатковы стан і дадзеўвае яго ў канец спіса. - HooksDispatcherOnUpdate актыўны пад час паўтарнага атрыбутавання.
useStateсупрацоўвае зupdateState, які праходзіць па існуючым спісе (фактычнаworkInProgressHook = workInProgressHook.next), обрабоўвае чергу незавершаных задач і вяртае новы стан.
Гэтыя прынцыпы таксама поясняюць, чаму вызов хукоў унутрь обрабоўчыкаў змян выклікае проблемы: калі обрабоўчык запускаецца, атрыбуванне вяліся ўжо, і дыспэтчер, які выклікае адказы, вяліся ў працэсе.
Як кожная група хукоў працуе внутршняя
Усі хукі выкарыстоўваюць аднойчынную лісту як базу, але сутэльна разняцца ў тым, што яны зберагаюць і калі выкананыя ўсе іх функцыі.
Хукі стану: useState і useReducer
Внутршняя частка useState — це useReducer з вбудованым редукерам, які альбо вяртае новую значэнне, альбо вызывае вашу функцыю-апдейтар з пярэйшай значэннем. Оба выкарыстоўваюць аднойчынны модэль выканання:
- Зберагчык. Хук зберагае базовы стан (пасляднюе значэнне, якое было зафіксавана) і кялеку для апдэйтаў, якая ёсць цыркулярным сшытамым списком чакаючых змян.
- Распадзел. Выкліканне функцыі-змянювальніка, напрыклад
setCount(c => c + 1), стварае об’ект апдэйта, які мае гэтую дзеянне, даджае яго ў кялеку і пазначае волокна як тыя, якім трэба выконваць роботу, прызначыўшы для яго канал (у старэйшых версіях для таго ж цыля выкорыстоўваліся часы заканчэння). - Рашэнне. Пад час наступнага атрыбутавання React праходзіць па кялеку і выконвае кожную дзеянне па порядку, ў результате чаго ствараецца новы
memoizedState. Апдэйты, каналы якіх не ўключеныя у текущае атрыбутаванне, захоўваюцца ўbaseQueueі выконваюцца пазней, што дазволяе зберагчыць порядак апдэйтаў незалежна ад ўсіх прыорітэтаў.
Гэта таксама прычына, чаму функціі апдэйтара ёсць безпечны выбор, калі наступны стан залежыць ад пярэднего: яны прыкладзеныся па абэрангу да таго стану, які калейдж вычысляў да гэтага часу.
Хукі эфектаў: useInsertionEffect, useLayoutEffect і useEffect
Кожны хук эфекта зберагае запис эфекта ў своем стане хука, які містыць функцыю налаштавання, функцыю чысткі і масэвую прычыннасці. Гэтыя записы таксама падключаюцца да адзінокага спісу ў updateQueue файбера, а эфекты, прычыннасці якіх змянілася, пазначаюцца, каб фаза адбыцьця знала, якія з іх трэба запускаць. Гэтыя тры хука разніцацца па часе выканання:
- useInsertionEffect выконваецца раней за эфектамі распараджэння, перад будзь-якім кодам, який можа чытаць распараджэння. Ён існуе для бібліятак CSS-in-JS, якім патрэбна можлівасць ранней інжектавання правілаў
<style>, каб стылі былі ўсталяваны ў момент вычыслення распараджэння, чаго дазволяе ухіліцца ад паўтаральнага вычыслення стылів. - useLayoutEffect выконваецца сінхрональна пасля таго, як React змяніў DOM, але раней, чым браузер успяў надрукаваць елементы. Главны поток блакуеца, пакуль эфект і яго адчысцэнне не завершыцца, таму ён падходзіць для вычыслення і налагоджэння вузлаў DOM прычымо да таго, калі корыстнік нічога не бачыць, і не падходзіць для болей складных задач.
MessageChannel, а як гэта не ўдаёцца — setTimeout), таму ён не спрычынае затрэмцоўкі візуальнага апдэйта. Згадайце, што калі апдэйт выкананы на адзінокай дзейсності корыстувальця, React можа выканаць пасыўныя эфекты раней, чым завершыць атрыбутаванне.Хукі для паўтароўнага выкарыстоўвання: useMemo і useCallback
Это кэшы, якія ухіляюцца ад дорогіх перысчытанняў або зберагаюць стабільныя канкрэтнасці межы перзапускаў.
- Храненне. Хук зберагае пару: кэшаваную значэнне і массив залежнасцяў, на адной з якіх ён быў вычыслены.
- Выконанне. Пры перыядэнтаванні React парабяруе кожную новую залежнасць з той, якая зберагліваецца, за дапамою
Object.is. Якщо всі пасуюць, фабрыка не вызываецца і вяртаецца зберагліваныя значэнне. Якщо хоць адна разлічыцца, React вызывае фабрыку, збераглівае новае значэнне і залежнасці, а пасля вяртае рэзультат. - useCallback ўзроўнаважны
useMemo(() => fn, deps): ён збераглівае об’ект функцыі, які вы паслали, а не значэнне, яке было створанае ўнаследкі её вызыву. У самай рэалізацыі React ён реалізаваны окрема, але повадкі тыя ж.
Пакалькі парабяраванне є поверхневым, залежнасць у вобразе свежаспадзярожанага об’екта або масіву практычна знішчоўвае всю кэш-систему.
Хукі для зменяючыхся значэнняў: useRef і useImperativeHandle
Refs зберагліваюць інфармацыю, якая не выкорыстоўваецца для рэндараўвання, такую як вузол DOM або ID таймаута.
- useRef — гэта самы просты хук у касоўнай базе. Пад час запуску ён стварае
{ current: initialValue }і зберагае яго як стан хука; кожны наступны рэндар вяртае абодва той жа об’ект. Змены ўcurrentніколі не паўтараюць процес апдэйта, таму не спрычыняюць рэндара. - useImperativeHandle дозволяе налаштаваць, што бачыць родзіч через ref, прыкрепляючы да яго савастныя методы. Унутрана ён працуе як
useLayoutEffect: ён выконваецца сінхрональна пад час апдэйта, таму хэндл ўжо готавы, калі запускаюцца эфекты родзіча. У React 19 функцыйныя компоненты можу прымецьrefяк звычную атрыбут, тамуforwardRefбольш не патрэбны для яго выкарыстоўвання.
Хук контэкста: useContext
useContext выдзеляецца тым, што ніколі не займае месца ў спісе хуков.
- Чытанне. Ён чытае значэнне з найбліжэйшага падходячага Providerа, які знаходзится вышэй за компонент, і запісвае гэты контекст у спіс залежнасцей файбера.
- Распространэнне. Калі значэнне Providerа змянюецца, React шукае нижэй за яным файберы, чыяя залежнасці включаюць гэты контекст, і плануе ў яных перыскаладчык. Гэта адбываецца нават тады, калі промежуточны компонент выйшае з процеса через
React.memoабоshouldComponentUpdate; самэўтаму мемаванне родніка не заходзіць аб’ектаў, якія викорыстоўваюць контекст.
Канкурэнтныя хукі: useTransition і useDeferredValue
Гэтыя два хукі ўпрымкваюць вас да системы канкурэнтных процесаў, дазволяючы перарваць трывалы процес складання інтерфейсу.
- useTransition вяртае
[isPending, startTransition]. Апдэйты, якія выкарыстоўваюцца ўнутрьstartTransition(() => setQuery(text)), атрымоўваюць прыорітет пераходу замест апдэйтаў з высокім прыорітетам. Якщо пад час атрымання рэзультата пераходу вы падаеце на элемент або нажмаеце клавішу, React адмовяецца ад обробкі дзержаных змэн, обрабоўвае апдэйт з высокім прыорітетам, а пасля пачынае пераход зноў з самага початку. - useDeferredValue абгортвае саму значэнне, а не функцыю для яго змены. React фактычна трывае два варыянты значэння: спачатку ён атрымлівае пракцэс выканання з пярэднім значэннем, ўтаманьчы такім чынам рэакцыйнас экрана, а пасля запланавае выканання з новым значэннем у фонавым режыме з нізкім прыорітетам.
Спецыяльныя хукі: useId і useSyncExternalStore
Дзеякі хукі рэдка з’являюцца ў коде прыкладнага програмісты, але яны ўжоць неабходны для автараў бібліятак.
- useId запобегае несувямствам у гідратацыі пад выкананнем на серверае. Ён гэтавае ID на адказ крыхты ў дрэве. Паколькі форма дрэва заўсёды аднаковая на серверае і кліянце пад час пачатковай гідратацыі, ID сувяршанае без якога-лібо глобальнага лячэння.
- useSyncExternalStore заменяе ручна напісаныя падпісы
useEffectда зовнішнях хранальняў, такіх як Redux або Zustand. Вы даеце ёму два функцыі: адну, якая рэгіструе слухальнік змян у хранальні, іgetSnapshot, якая вяртае текущую значэнне хранальні. React чытае этот снімак пад час выканання, і якщо хранальні змянюецца пад час выканання, ён перывыканае сінхронна, так што жадная частка UI не паказвае іншую версію хранальні чым іншая. Цэе запобегае розрывам, якія іншым чыным моглі бы выйсці ад канкурэнтнага выканання.
Галоўныя выводы
- Стан хука — это сяродзеўны список у фібры, які падтрымліваецца толькі па порядку вызоваў; кожны правілле хукав прызначаны для таго, каб гэты порядак заставаўся аднаковым праз усе рэндары.
- Фібра ператварае процес рэндаравання на цикл, які можна перарваць, а двойная буферазая дазволяе React падготавіць новую структуру без змены таго, што ўжо апісана на экране.
- Фаза рэндаравання можа выконваліцца многа разоў і павінна заставацца „чыстай“; фаза зберагання выконваліцца адна раз, сінхронна, і самэ ў гэтай фазе запускаюцься эфекты.
- Дыспэтчеры пояснююць як раздзеленне на монтаж/апдэйт, так і адхыленні, калі хук вызываецца па за межами процесу рэндаравання.
- Знаёмасць пра тое, дзе кожны хук зберагае своі даны і калі выконвуецца яго код, дапамагае легчэ выбраць правы момент для запуску эфектаў, падтрымваць эфектыўнасць мемаізацыі і разумець контэкст асабліва канкурантных апдэйтаў.
Супаўязаная літэратура
- Дзе useState зберагае сваю значэння: React Elements протык Fibers — чаму функцыйны компонент React забывае ўсё між вызовамі, чаму элементы не можаць зберагаць стан, і як поле memoizedState фібраў падтрымляе значэння useState жывымі.
- Тыпаванне React Hooks: useState, useEffect, useReducer і спецыяльныя хукі — дазнаёцесь, як правільна працаваць з useState, useEffect, useReducer і спецыяльнымі хукамі ў TypeScript, а таксама калі выбор TypeScript замест звычнага JavaScript дэйсна ўзгоджваецца.