Розумеўць React Lanes: як бітмаскі кодуюць прыоритет апдэйтаў
Дазвольце дазнацься, як React закодавае калькольныя прыорітэты апдэйтаў у аднам цэлым числе за дапамою масак бітав, і чаму для планавання викорыстоўваюцца бітовыя аперацыі замест простых логічных флагоў.
Усё-такі не булевыя значэння
У паказванай раней частцы вы бачылі, як lanes і childLanes функцыонуюць так, нібы яны былі простымі флагамі. Адно полье на волокні Counter пераключалася з порожняга на заполненае, тое ж сама пераключэнне відбывалася на кожным верхнім волокне, і працэўка па дрэве базавалася на гэтых пераключэннях, каб вырашыць, якія галузі варта ўвайсці. Для гэтага абяснення нічога іншаго не мела значэння: полье было або порожняе, або ні.
Але гэтыя описы не паказваюць, што на самай працэ ўтримваецца ў гэтых полях.
{
type: Counter,
memoizedState: { count: 1 },
lanes: 16,
}
lanes храніць число, а не true чы false — рэальны цэлык, які зберагаецца на волокні. Якбы React патрэбна была толькі інформацыя пра тое, „чы ўсё яшчэ у процесе, так чы ні“, простае булева значэння цалкам бы вырашыла гэтыя питанні. Але React выбрала ўсё іншае.
Тадзе чаму цяжыба являецца нейтарным выборам для пазначэння "гэты волокна ў чаканні выконання задач"?
Што такое бітмаска
React прыносіць групу сталярых значэнняў, створаных саме для гэтай меты, і як толькі ўпорядкуваеш их, адразу ж пазнаёмся з адказам.
SyncLane = 1 = 00000001
InputContinuousLane = 4 = 00000100
DefaultLane = 16 = 00010000
TransitionLane = 64 = 01000000
Запісаныя у двоековай формате, кожна з сталярых значэнняў містіць роўна адну 1, прычаму гэтая 1 знаходзіцца на аднаковай пазыцыі для кожнага з іх. SyncLane займае першую пазыцыю; DefaultLane — пяту. Ніхто з іх ніколі не знаходзіцца на той самай пазыцыі. Такая структура вядома як бітмаска — цяжыба, якой значэнне не мае значэння з числовай точка зору, а служыць радой незалежных выключнікаў «выкл./включ.», по аднаму на кожны біт, усе якія зберагаюцца разам у аднай ячэйке.
Калі файбер Counter паказвае lanes: 16, гэта не значыць, што «гэты апдэйт коштае шысцьнацца единакоўных елементаў». Гэта означае, што біт, які належыцьDefaultLane, ўвімкнуты, а всі іншы біты застаюцца выключанымі. Гэта число служыць контейнерам для флагоў, а не колькасцю, якая трэба вимерваць.
Што будзе, калі аднаму файберу патрэбна будзе адразу стежыць за больш чым аднім з гэтых флагоў?
Спалучэнне двух каналаў за адной операцыі
Уявіце файбер, які ў той жа момант прыме два розныя типы завершальных задач — звычайны апдэйт і той, які спецыяльна пасунуты на нижэйшую прыорітетнасць. React з’едначае гэтыя два бітмаскі за дапамою операции OR па бітах: гэта операцыя вырашвае, чы ў рэзультате біт будзе 1, калі ён уяўліваны хоць у адной з выхідных цифр.
00010000 (16, DefaultLane)
| 01000000 (64, TransitionLane)
-----------
01010000 (80)
Пакалькі DefaultLane і TransitionLane кожны маюць пазіцыю біта, якой не трапляецца ў іншых ленях, таму ўзаемна ўзьязджанне іх проста дадае два 1 у рэзультуючы числовы значэння, без таго, каб адны з іх стараўся стырзець другі. Рэзультат, 80, являецца новым значэнням lanes, і якщо перакануць яго знова у двойчатную систему, можна пабачыць, што оба первісныя біты застаюцца ў яму, такімі ж, якімі былі.
Якщо паглядзець на сама число 80 — што ж яно насправды паказвае?
Чаму сама цифра 80 нічога не паведамляе
Сама по сабе 80 нічаго не паводзіць. Такую значэнне не чытаюць як звычную цифру, яе аналізуюць паштоўку за паштоўкай, выкарыстоўваючы аперацыю AND па бітах, якая залеўляе 1 у пазі толькі тады, калі ў обох пораўнюваных числах вэсці 1.
01010000 (80)
& 00010000 (16, DefaultLane)
-----------
00010000 (non-zero → yes, DefaultLane's bit is set)
01010000 (80)
& 00000001 (1, SyncLane)
-----------
00000000 (zero → no, SyncLane's bit is not set)
Значэнне лянку ніколі не аналізуюць сама по сабе. У замен ставяцца конкрэтны вопыт пра адну лянку, і адпаведзь надходзіць або як ноль, або як не-ноль. Самэ гэта і адбывалася на самай сутнасці ў логіцы праходжэння па дрэве, якая была рассказана раней: кожны раз, калі аналізавалася вялічына childLanes фібры, каб выявіць, чыя галузь варта праходзіць далей, выкананы быў той самы пераказ AND па бітах для той лянкі, яку ў тым час аналізавалі.
Тады зноў да асновнага пытання: чаму ўзагале представляць гэта як число, а не проста як булевы флаг?
Чаму проста не выкорыстаць булевую значэння?
Булевая значэння можа адпаведзець толькі на адны тип пытання: чы є якае-небудзь нераспрацаванае заведамства, так чы інакш. У яе няма можлівасці выразіць „чакае звычайная апдэйт, і, окрэмна, чакае таксама заведамства з ніжшым прыоритэтам“, таму што булевая значэння несе толькі адну бітовую інфармацыю. React на самай працэўніце патрэбна можлівасць адзначаць кальколькі розных катэгорыяў нераспрацаваных заведамстваў адночасна, і ёй патрэбна можлівасць пераканацца ў наяўнасці будзь-каго з іх без вплыву на стан іншых.
Маска бітова адмініструець два выклекты ўодночас. Адзін числовы поле можа несці калькі знакоў паралельна, таму што кожны знак займае свой сабэт біта. А перацэнка адного знака чы ўжо сумаванне калькі знакоў — гэта адна дышча операцыя CPU, AND чы OR, а не што-небудзь, што вымагае праходжэння па структурэ дадзенаў чы зберагчы окалічную булевую зменную для кожнага можлівага рангу прыоритэта. Гэта ўсё праблема, чаму lanes зберагаецца як цэлычая лічба, а не як true чы false: гэта не фіксуе, чы існуе робота, а фіксуе самэ прыгожыя відэры роботы, усе ўодночас, практычна без жадных затрат.
Што яшчэ не было адпаведзена, так гэта чаму актуалізацыю ўжо з самага пачатку трэба спрыята як роботу ніжшага прыоритэта.
Чаму актуалізацыі важка прыоритэт
Разглядзім кантэнер для пошуку, створанный як контролюемы компонент, дзе кожны нажаты ў клавіатуру перфільтруе спіс з дзесяці тысяч элементаў, пры чым сам фільтрацыйны процес адбываецца прымусова ў тэле функцыі компонента.
function SearchableList({ items }) {
const [query, setQuery] = useState("");
function handleChange(e) {
setQuery(e.target.value);
}
const filteredItems = items.filter(item => item.includes(query));
return (
<div>
<input value={query} onChange={handleChange} />
<ul>{filteredItems.map(item => <li key={item}>{item}</li>)}</ul>
</div>
);
}
Увайдзіце адны симвал. Выконваецца handleChange, якая вызывае setQuery, і гэта запланавае аднойчы перерасчыт SearchableList. Калі React нарэшце обрабоцуе гэты перерасчыт, ён вызывае функцыю SearchableList як адну неперарваную, сінхронную вызову з пачатку да канца. У якомусь месцы ўнутры гэтага адного вызову .filter() павінен працаваць над усіма дзесяцьма тысячамі элементаў, пры чым функцыя толькі пасля гэтага можа вернуць свой новы JSX.
Выклік функцыі пад час её выконання не можа быць перываны ў середзіне самага яе коду. Можлівасць перывання, яку з’яўляе файбер, дзейсніцца на межы між файбрамі, а не пад час лінійнае выконання функцыі, якая ўсё ще трывае. Таму у React немае можлівасці адразу паказаць апошнюе значэнне вводу, зберагчы яго чы ўвайсці пікселі на экран, пакуль не завершыцца выклік той функцыі, уключаючы ўсе етапы фільтрацыі. Наступны натиск клавішы залишаецца у черге пасля яго, не таму, што два окольныя апдэйта конкуруюць, а таму, што оба завдання ўключеныя ў рэзультат таго ж аднаго выкліку.
Рашэння — разбіць гэта на два абсалютна незалежныя апдэйты, кожны з якіх можа маты свой саўственны ранг прыорітэта.
function SearchableList({ items }) {
const [query, setQuery] = useState("");
const [deferredQuery, setDeferredQuery] = useState("");
const [isPending, startTransition] = useTransition();
function handleChange(e) {
const value = e.target.value;
setQuery(value);
startTransition(() => {
setDeferredQuery(value);
});
}
const filteredItems = items.filter(item => item.includes(deferredQuery));
return (
<div>
<input value={query} onChange={handleChange} />
<ul>{filteredItems.map(item => <li key={item}>{item}</li>)}</ul>
</div>
);
}
setQuery і setDeferredQuery тепер ўтвараюцься як два адзінольныя вызовы, кожны з яых запускае сваю сабе аднаўленне і кожны мае свой унікальны ідэнтыфікатор. Аднаўленне, яке выйшла з setQuery, ёсць недорогае, адтолькі што не выкалічваецца ніякое фільтраўванне, таму яна быстра завершваецца і поле вводу застаеся чутлівым да набору симвалаў. Аднаўленне, яке выйшла з setDeferredQuery, — гэта тое, якое насправды выкананае .filter(), і яно несе параметр TransitionLane, таму што ёно абгорнутае ў startTransition, хук, які актываўае гэтый шаблон адложанага аднаўлення. Якщо вы наберазеце ўтолькі іншы симвал, пакуль яшчэ трывае цяжкае аднаўленне з фільтраўванням, React пераглядае lanes, знаходзіць аднаўленне вышэйшай прыоритэтнасці, якое чакае, і адкладае незавершаную работу з ніжэйшай прыоритэтнасцю, каб спачатку адпрацаваць набіраны симвал. Гэта можліва толькі таму, што гэтыя два аднаўленні тепер фіксуюцца незалежна.
Кожны з іх мае свой сябе пазнаку прыоритэта, а не ўскладненыя ў адны вызов функцыі, якая обрабоцвае оба заведамства адразу.
У гэты момент цікава запытка — чакаецца, што разработчыкі будуць ручна прызначаць гэтыя значэння прыоритэта, апдейт за апдэйтом.
Прыоритэт здарожна ў автаматычным режыме
У большасці случаў — няма. React зазвычай выбирае шлях на адной з основ таго, што спачатку вызвало апдэйт, а не таму, што хтось намеравана выбраў які-небудзь шлях. Гэта рашэнне прыймаецца ў системе змаганняў React, якая ведае, які тип змагання браузера обрабоцваецца самэй у момент выканання setState.
Прыблізна па рангу, ад найбольш няцерплівага да менейшага:
Discrete events (clicks, key presses, form submissions)
Continuous events (scroll, drag, pointer-move)
Default (timeouts, resolved promises, no special context)
Transition (explicitly deprioritized via startTransition)
Idle (only when nothing else is pending)
Выкліканне setState заўтрыч у працоўніку кліка автаматычна ставяе яго на верхнюю частку такога рангу. Выкліканне яго з працоўніка прасування ставяе яго на адны ўрадзень нижэй, адтолькі што кожна змянена пад час прасування вважаецца за высокапрыоритетную, што можа відбывацца дзесяткі разоў за секунду, і гэта сама по сабе можа спрычыніць проблемы з адгукам. Змянена, запусканая пасля выпалення promise або таймауту, без наявнасці супакойліваючага контексту, выкарыстоўвае значэння DefaultLane за замовчаннем. Ядыныя ручныя настройкі, да якіх практычна мае доступ разработчык, — це startTransition, і його дзейснісць є вузкая: або змянена праходзіць па звычайным шляху, і React выводзіць яе прыоритет на адной заснове контексту, як і для всіх іншых элементаў, або ёй надаецца спецыяльны обгортак, што прымусвае яе пераводзіцца у TransitionLane, незалежна ад таго, што на самай працоўнік яе запускало. useDeferredValue па суты є легкім обгортакам, створаным на адной з тых сэрвісаў. Ніхто не задае значэння
fiber.lanes = 64 — гэта можна зробіць безпасова, і няма жадных прычын для цього. Є адна застерэжніца, якую варта згадаць перш чым завершыць: фактычны кодбаза React включае блізкая да трыдцяти розных констант для каналав, а не толькі чатыры. Большасць з іх існуе выключна для внутрашняй бухгалтерскія утриманні, а не для практычнага выкорыстання як новых рэвалютацый прывілеяў, якія дапамагаюць незалежна стежыць за калькамі, а не злучаць іх у адну спяльную, неразмежаваную категорію. Пяць узначэных вышэй рэвалютацый ёсць тыя, якія варта запам’ятаваць як ментальную модель. Усе іншае — цэлкам деталі рэалізацыі, якія знаходзяцца пад структурой, яку вы вже розумеете.
Чатыры элементы, адны механізм
Якщо адаптаваць усё разам, то гэта тое, што на самай працо відбываецца пад час перзапуску адрадзіравання, з самага пачатку да канца. Стан зберагаецца ў файбары, таму што файбар — гэта об’ект, які прыжывае пасля аднае пасэў адрадзіравання, на вядомаць ад функцыі компонента, якая забывае ўсё между вызовамі, або ад элемента, які ў кожны раз знімаецца і перзабудовваецца. Файбар выглядае як два сувязаныя варыянты — текущы і той, які ў процесе стварэння, якія з’едынаюцца за дапамогою паказвальніка alternate, таму што для будзь-яга порэвання неабходны рэальныя знімкі «до» і «пасля», якія стояць па боку ад боку. Вызов setState ніколі не вплывае безпасяродна на жадны з файбароў. У замен ён залишае запіс у кяле і відкладае рэальную работу, тады як пазначаны набор каналоў дае можлівасць пазнейшаму праходзе ў дрэве зразумець, якія галузі насправды вартае адвідаваць. А гэты пазначальнік ніколі не быў простым
Паўзнік — гэта, прыгожва, маска бітав, яка несе достатню колькасць закодаванай інфармацыі, каб адна натыкання на клавішу дазволіла праскочыць усю роботу, яка ўсё яшчэ чакае на свою рэчы ў черзі.
Чатыры разныя ідеі, але яны з’едынаюцца ў адны механізм. Кожная з іх стае цэлкам зрозумелай толькі пасля таго, як вы вучыліся пярэдней. Якщо рассматраць іх окрема, кожны з эых элементаў ператвараецца ў факт, які можна запам’ятаваць, без адгукнення да логіки, якая яго на самай працэ падтрымлівае.