Чаму setState ніколы не перзапускае атрыбуты на месцы: планаванне апдэйтаў у React
Следзіце за апдэйтом стану React, які выходзіць з об’екта апдэйта ў черге, через пазначэнне шляхоў та планаванне да цыклу выкарыстоўвання, який спачатку обрабоцвае корэнны элементы, пры чым ігнаруеее непаўнажлівыя паддрэсіны.
Крыніцы: The Fiber Architecture In React Part 3: What Actually Happens When You Call setState
Практычна кожны разработчык на React мае таямніцэ: ён прыпускае, што вызванне функцыі для змены стану, такой як setCount(count + 1), перзначае компонент прымусова, яшчэ до выканання наступнай лініі коду. Аднак так не ўсё, і разлік межа вызванням та перзначэнням компонента паводзіцца да концепцыі групавання операцый, па якой некалякуючыя між сабою компоненты застаюцца недастрымленымі, а чытанне стану сразу пасля його змены вяртае старую значэння. У этай статыце працуецца з адним толькі обновленням у рамках архітектуры React Fiber, ад моменту його запісу да моменту, калі нова значэння прабачваецца ў стане компонента, ўжо каб вы моглі зрозумець, што на самай працэ функцыі для змены стану.
Компонент пад мікроскопам
Дастатнька простая лічылка, каб паказаць весь механізм. Яна зберагае адну вялічыню стану і збільшае ёё пасля натыкання на кнопку.
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Выклік setCount(count + 1) выглядае як звычная сінхронная функцыя, таму ўсё прыродна думаць, што React зноў вызывае Counter() як безпосереднія наследка гэтай лініі. Але нічага подабнаго не выходзіць. Функцыя-сеттар вяртаеся, нічога не адрасуючы. У замяне ёна фіксуе запит і просіць абы робота была выконана пазней.
Першы крок: сеттар фіксуе апдэйт
Калі запускаецца setCount(1), React выдзеляе ў памяці маленькі об’ект апдэйту, який описвае гэты запит. Канцэптуальна ён выглядае так:
{
action: 1,
next: null,
}
Поле action храніць тое, што вы перадалі, у даным случае значэнне 1. Поле next існуе таму, што адна зміна рэдкая развязвае всю проблему. Выклікайце функцыю-застаўлячы тры разы ўнутрь адной обрабоўчай функцыі, і React стварае тры об’екты змян, спаяныя за дапамогою next, утвараючы маленькі список, прыўязаны да компонента. У рэальным коде React змена таксама несе інформацыю пра свой прыорітет, а список усіх запланаваных змян храніцца як цыркулярная структура, але сутнасць заключаецца ў чергавай выпалоўкі задач, а не у паўторнай відраслівцы компонента.
У спрасцаванай модэлі фібры змена прыўязываецца так:
{
type: Counter,
memoizedState: { count: 0 }, // still 0, untouched
updateQueue: { action: 1, next: null }, // the note, just added
}
Зверніце увагу на тое, што не зменілася. Збережаны стан все ўсё показвае 0, на экрані таксама відображаецца 0, а функцыя Counter() так і не была вызвана. Єдыная разліка — тепер об’ект чакае ў черзі. (Для точнасці: за дапамогою хуків React зберагае чергу кожнага хука ў елементе яго внеску у список хуків файбера, а не безпосередньа ў поле пад назвай updateQueue; memoizedState у функцыйнай компоненте-файберы — гэты самы список хуків. Выказаная вышэй спрощаная модель служыць для навчання і зберагае ту поведінку, якая тут мае значэння.)
Гэта таксама пояснюе класычны сюрпрыз. Якщо вы запісаеце значэння count ведаў пасля вызову setCount, вы побачыце старое значэння, таму што функцыя компонента, якая працюе з count, ўсё ўсё не была запускана знову.
Другі крок: scheduleUpdateOnFiber пазначае шлях і запрашоўвае виконання задач
Як толькі апдэйт практычна запісваецца, React вызывае внутранюю функцыю пад назвай scheduleUpdateOnFiber. У яе є два окремыя завадзівкі, і ўсуненне іх адступленняя спрыяе лепшаму розумэнню застаўшагуся процесу.
Пазначэнне шляху да корана
Спачатку React праходзіць ад файбера Counter вгору через кожны пункт-родзіца, пакуль не дасягне корана дрэва. На кожным прадку на шляху ён задае поле пад назвай childLanes, якое фактычна означае "адзін з элементаў пад мной мае незавершаныя задачы". Сам файбер Counter таксама мае супаўзнае поле lanes, якое означае "у мяне таксама є незавершаныя задачы">.
Этыя польны не ўжо простыя булевыя значэння. Це маскі бітов, якія кодуюць прыоритет, што ў самае сабе являе абсалютна окремую тэму; дакладней чытайце як React lanes выкарыстоўваюць маскі бітов для кодавання прыоритету апдэйтаў. Для ціх поясненняў дастатньа знаць, што яны пераходзяцы з пустага стану ў непусты, залишаючи «след з печакі» ад апдэйтаванага компонента да верхней часткі.
Відкладэнне фактычнага атрыбутавання
Паўторна частка: React просіць, каб рэндарынг адбываўся якомога шчэжэй, а не ўнутры вашага обробніка змян. Точны механізм залежыць ад прыоритэта. Робота, якаю обробляе планавальнік React, зазвычай дасылаецца через MessageChannel (з setTimeout як альтэратывай у средах, дзе яго няма), таму ёна адбываецца пазней у цыкле змян. Для окремых, высакапрыоритетных змян, такіх як клікі, сучасныя версіі React зазвычай виконваюць цю роботу як мікрозадачу ведаўна пасля таго, як заканчыцца обробнік, тое ўсё пасля вашага коду, але шчэжэй. Точная стратэгія зменялася между выданнямі, таму спрыймайце деталі як спецыфічныя для кожнай версіі.
Незалежна адзінства канала, наследкі ўсё тыя ж: адрасаванне дакладна адкладаецца пакуль ваш хэндлер не вернёўся. Самэ гэта дазволяе выкананне пакетных операцый. Тры вызовы setCount у аднам хэндлеры ствараюць тры запісаныя апдэйты, і адна пазнейшая процедура адрасавання обрабоцвае ўсія іх разам, замест таго каб адрасаваць тры разы.
Трэці крок: адрасаванне завжды пачынаецца з корні
Калі нарэшце выканана запланаваная робота, яна не пачынаецца з Counter. Яна пачынаецца з корневай волокна, спецыяльнага вузла над усім вашым прыкладам, який быў створаны калі вы вызвалі ReactDOM.createRoot. Гэта адбываецца незалежна ад таго, на якой глыбі дрэва выйшла апдэйт.
Праця, яка пачынаецца з корневага элемента, выглядае дорогай, але гэта не значыць, што трэба адвячараць кожны падэль. След childLanes, заданы ў другім кроку, якраз і є тым шляхам, які выкарыстоўваецца пад час праходжэння. Пра кожны падэль React пераканальваецца, чыя ў яго паддрэве є незавершаныя задачы. Якщо іх немае, React прыпыняе працу і праскаквае цэлы паддрэв, не спускаючыся ў яго. Анкесторы на пазначаным шляху праходзяць без додатковых затрат, а дакладна адвячараюцца толькі галузі, якія вяду да незавершаных задач.
Це цыкл, а не рекурсія
Процес праходжэння реалізуецца як цыкл, а не за дапамою глыбокай рекурсіі:
let node = rootFiber;
while (node !== null) {
node = processNode(node); // moves to node.child, node.sibling, or up via node.return
}
Кожны элемент сіткі мае тры паказывачы: child — для сваго першага дзецячага элемента, sibling — для наступнага элемента на там жа глыбі, і return — для ягоя бацьківскага элемента. Шырокія гэтыя звязкі дазволяюць пераходзіць да дзецячых элементаў, да суседніх элементаў на там жа глыбі і зноў назад, калі завершаецца паводак, шаг за шагам. Не існуе масіва дзецячых элементаў, якія можна було бы пераглядваць. Паколькі пазицыя ў сіткі зберагліваецца ў зменнай, а не на стакане вызываў, React можа націснуць роботу между елементамі і праз кантакт яе адноваць пазней, што ёсць адной з базавых прычын паралельнага атрыбутавання.
Чаму несвязаныя компоненты застаюцца недастрымленымі
Это механізм, які стоўчы за эфектам, які вы, верагацей, вядомлі. Введэнне тэксту ў поле вводу апдейтуе толькі тую частку дрэва, якая адпаведае цім стану. Далёкі, несвязанный компонент не перерабляецца і нават не адвідуецца, таму што жадныя апдэйты не пазначылі його галузь. Адхіленне ад перерабляэння не ўскладненая оптымізацыя, даданая пазней; гэта вытыкаецца непасрэдна з адступлення па шляху childLanes.
Памятаце пра гэтыя межы. Компонент на пазначанай дорозе, у якога самага є незавершаная робота, будзе перерабляцца, і за замовчаннем таксама будуць перерабляцца ўсі його наследнікі, пазначаныя чы не, якщо толькі ўони не зберагліся у памяці. Шлях паведамляе React, куда ісці; ён не заважае перерабляючамуся родзіцу перерабляць своіх дзецей.
Чэтыры ўрад: падходжанне да Counter і викорыстоўванне кялі
Калі працэздольг дасягне Counter, прыемае ўладу механізм двойнага буферавання. React пераказвае паказвач alternate у чынным фібры і альбо паўтарна выкарыстоўвае існуючы фібры, які знаходзіцца ў процесе обробкі, альбо стварае новы. Самэ ў гэтым копіі, якая знаходзіцца ў процесе обробкі, вырахоўваецца новы стан.
Тепер шаранга нарэшце прачытана. React обрабоцвае чакаючуюся апдэйт, застосоўвае дзеянне да паканальнага стану і запісвае рэзультат у memoizedState волокна, якое знаходзіцца ў процесе адрабаткі. У гэтым кроку фактычна вызываецца Counter(), і useState вяртае новую значэнне. Якщо кілька апдэйта было падацелена ў шарангу, яны застосоўваюцца па порядку; функцыйныя апдэйты, такія як setCount(c => c + 1), кожны з яных отрымае рэзультат паканальнага апдэйта, таму яны правільна працуюць, калі ўжо ўтвореныя апдэйты об’еднаны, тады калі setCount(count + 1) вызывацца трохы разоў, выходзіць толькі адна дадатковая збірка.
// work-in-progress fiber, right after processing the queue
{
type: Counter,
memoizedState: { count: 1 }, // finally updated, using the queued action
updateQueue: null, // consumed, nothing left pending
alternate: Fiber A,
}
Уразряднік зараз пусты, а новы стан знаходзіцца на волокне з працэю. З’явіліся тры разныя механізмы: апдэйт з рядка першага, пазначаны шлях і праця з корнем з рядкаў другага та трэцьяго, а таксама пара волокен “тэкуча/у працы”, якая зберагае версіі “до” та “пасля”. Чырвонейшыя деталі, якія продовжаюцца праз процес супрааварызаціі та фіксацыю DOM, можна пазначыць за лінкам “Трасуванне вызову React setState з апдэйт-разраядніка да фіксацыі DOM”.
Полная последовальнасць
Якща рассматраць усе паэтапна, адзін клік прыносіць да пераходу праз следуючыя стадіі:
- Функцыя-обрабоўчык кліку вызывае
setCount, і React стварае об’ект апдэйту та даджае яго ў разраяднік компонента.
scheduleUpdateOnFiber задае значэння lanes для элемента Counter і childLanes для кожнага яго предка аж да кораня.child, sibling і return, пры чым прахоўваюцца паддрэвы, у якіх няма запланаваных роботаў.Counter React падготавляе волакно з незаканчанай роботой, обрабоўвае запісаныя апдэйты і вычысляе новы стан.Нічога з гэтага не выконвуецца на тыяй лініі, дзе вы вызвалі функцыю-зменяйшык. Усё выконвуецца пасля таго, у структурэ, чыя поля існуюць саме для падтрымкі гэтага відкладанага, можлівага да продажы процэсу.
Галоўныя выводы
- Канал налагоджэння стану дадзеўкае апдэйт і запланавае выконанне; ён ніколі не выкарыстоўвае сінхронную обработку ў вашым хэндлеры.
- Чытанне стану сразу пасля яго налагоджэння вяртае старую значэнне, таму што компонент яшчо не перзапускаўся.
- Групаванне ёсць паўнастай наследком адкладэння: кожны апдэйт, дадзеныя раней за пасляўрадку адрасавання, обрабоўваецца заодно.
lanesіchildLanesпазначаюць шлях ад апдэйтаванага фібра да корню, чым React можа праскочыць незмененыя паддрэсі.- Цыкл адрасавання выкарыстоўвае паказывачы
child,siblingіreturnзаместа рекурсіі, што дазволяе React павярнуць і продактываць работу. - Компонент, які перзапускаецца, за замовчаннем прыгрупавае своіх дзецяў; след каналаў лімітуе тое, дзе React шукае, а не тое, што выканаўць родны компонент.