Галоўная / Артыкулы / Аналіз вызову React setState з калэндара апдэйтаў да выканання ў DOM

Аналіз вызову React setState з калэндара апдэйтаў да выканання ў DOM

Следзіце за апдэйтом стану React па кроках через чаргу апдэйтаў Hook, планавальнік, фазу атрыбутавання, супрацоўку і зберагучы апдэйт, і разумейце, чаму стан ніколі не зменшыцца адразу.

2432 слоў

Практычна кожны разработчык на React зрэшча пісвае функцыю для змены стану, а пасля — console.log, і дывуецца, калі бачыць, што выдрукоўваецца старая значэнне. Назва setState можа супакоўваць, што вядзецца мглевае прызначэнне, але React так не дзейнае: вызов сетара фіксуе запит на будучы стан, і перад тым, калі ўсё праказуецца на экране, праходзіць цэлы ланцоўка ператварэння. У этай стацыі адступленае выкананне апошней змены праходзіць через усю гэтую ланцоўку — ад очаквальнай чергі змэн у хуке, па шэдуліраванню, атрыбутам, рендарыну, супраўажэнню і фазе зафіксавання. Калі вы зможаце уявіць кожны ўзлок, некалькі явароў, якія спачатку здаюцца дзівнымі, стануць прыемлівымі: чаму стан выглядае асінхронным, чаму калькі змэн можа скасавацца ў адны рендар, чаму React іноды прахоўвае певныя крокі, і чаму існуюць функцыональныя змены.

Фрагмент, які дывуе всіх

Уявіце сабе перагляд коду, дзе гэта выглядае абсалютна логічна:

setCount(count + 1);

console.log(count);

Апэктаваець, што консоль паказае збільшаную значэнне. Але там паказуецца пярэднее значэнне. Ёсьць тая ж ситуацыя ўнутрь цэлага компонента:

function Counter() {
    const [count, setCount] = useState(0);

    const handleClick = () => {
        setCount(count + 1);
        console.log(count);
    };

    return (
        <button onClick={handleClick}>
            {count}
        </button>
    );
}

Пад першым клікам багато разработчыкаў спадзяюцца побачыць:

1

Тое, што на самай працоўцы паказуецца ў консалі, — гэта:

0

Прычына ў тым, што React яшчэ не адрабатаў код занова. У момент, калі выконуецца console.log, React практычна толькі получыў запит. Нічога з лісты чакання яшчэ не было застосавана, функцыя компонента яшчэ не была вызвана, а DOM не змяніўся. Код, які вы выкананяеце, належыць да текущага адрабатвання, і ў рамках гэтага адрабатвання count ёсць звычайная сталяя, якая была заданая ў момент выконання функцыі. Нічога не можа яе перазначыць.

Это першыя змены ў ментальной модэлі: функцыя-зменнік не мутуючы значэння зменной стану, якае вы чытаете. Яна проста плануе виконання задачі для React пазней.

Што зберагае React, калі вы вызываете функцыю-зменнік

Калі вы пішаце:

setCount(count + 1);

Бяжыць падумаць, што React внутршняя часткаю робіць нечыга такога:

count = count + 1

Цього не выканае. React стварае об’ект з метаданнымі апдэйту і даджае яго ў чергу, якая належыць конкрэтнаму Hook-у. Канцэптуальна ситуацыя пасля натискання кнопкі выглядае так:

Current State
      |
      ▼
count = 0
      |
      ▼
User Clicks
      |
      ▼
setCount(1)
      |
      ▼
Update Queue
[ Update: 1 ]

Сам стан застаецца недастрыжаным. Усё, што зрабіў React, — гэта запісаў сабе прыметку: наступны раз, калі будуць обробляцца апдэйты для гэтага Hook-у, count павінен стаць 1.

Чаму апдэйты праходзяць через чергу

Черга становіцься зрозумелай, калі калькі апдэйтав прыходзяць праз маленькі час. Разглянем кэшер, який вызывае функцыю-зменнік тры разы:

const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
};

Тыя, хто новачак у React, можаць спадзецца, што адна кліка прынесе:

count = 3

Рэальны рэзультат ёсць:

count = 1

Усе тры вызовы былі выкананы пад час аднаго і тога ж рэндару, і гэты рэндар паказаў:

count = 0

Таму кожны count + 1 дае той самы чысла, і React прымае тры ідэнтычныя запросы:

setCount(1)
setCount(1)
setCount(1)

Такім чынам у кяле ёсць тры элементы, якія всі паведамляюць "значэнне = 1":

[1]
[1]
[1]

Обробка іх па порядку таксама завершваецца:

1

і не:

3

Тая ж логіка прымаецца, калі значэння разныя. Падазром, рэндар выканае гэтыя тры вызовы:

setCount(1)
setCount(6)
setCount(4)

Тады кяле містіць:

[1]
[6]
[4]

Кожны элемент абсалютна заменяе поперадні стан, таму пасля обробкі значыць толькі апошні, і рэзультат становіць 4. Простыя значэнні ўсё толькі заменяюць, а не являюцца інструкцыямі для далейшай працы з тым, што было раней. Самэ гэтая обмежэннае ўмова являецца прычыной існавання функцыйнальных апдэйтаў.

Функцыйнальныя апдэйты вырахоўваюцца на базе найсвежэйшага стану

Тепер зменіце хэндлер так, каб кожны вызов перадаваў функцыю:

const handleClick = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
};

Гэты раз у чергавой лісты ёсць што-та болей сэроднее трох простых програм, чым трох значэнняў:

prev => prev + 1
prev => prev + 1
prev => prev + 1

Калі React обрабоўвае чергавую лісту, ён падае выход кожной функцыі на вхід наступнай:

0 → 1
1 → 2
2 → 3

і канечны стан ёсць:

count = 3

Разлік у тым, што функцыя апдейту не захопляе значэння з текущага рендару. Яна описвае, як вылучыць наступны стан з таго стану, які ў React у момент обробкі таго елемента. Выкарыстоўваеце такі формат, калі новы стан залежыць ад пярэднего, особліва калі калькі апдэйтаў можаць быць запісаныя разам або калі апдэйт выконваецца ўнутры калбека, створанага пад час болей старога рендару.

Полны жыцёвы цикл аднаго апдэйту

Вяртаючыся да чергі, ступіце за адним клікам, який вызывае:

setCount(count + 1);

Спрасцаваны відблік усьго, што выходзіць далей:

User Click
     |
     ▼
Create Update
     |
     ▼
Place Update Into Queue
     |
     ▼
Notify React Scheduler
     |
     ▼
Schedule Render
     |
     ▼
Render Phase
     |
     ▼
Reconciliation
     |
     ▼
Commit Phase
     |
     ▼
DOM Updated

У наступных раздзелах практычна расказана пра кожны этап.

Этап 1: ствараецца запись апдэйту

Выклік setCount(1) ніколі не рэндаруе ўсё безпасяродна:

setCount(1);

React стварае запись апдэйту, якую можна уявіць сабе як прыметку з напісам:

Apply this update later.

Эты рэкорд прыўязаны да внутраняга стану Hook-а. Данные Hook-а знаходзяцца разам з Fiber-ам компаненту, внутранім вузлом, які React зберагае для кожнай інстанцыі компаненту, а таксама там жа знаходзіцца календар выконання апдэйтаў:

Fiber
   |
   └── useState
           |
           ├── Current State
           └── Update Queue

Гэтая прычына таксама змушвае выклікаць Hooks у той самы порядак пад час кожнага рэндарування: React знаходзіць стан і календар кожнага Hook-а па яго месцу ў гэтым списку.

Шаг 2: React плануе выконання задач

Калі є апдэйт, React павінен вырашыць, калі яго обрабатваць. Гэта заведамасць планавальніка. React не обавязкова рэндаруе моментальна пасля выклікання setter-а; ён балансуе час адказу і неабавязковую роботу.

Уявіце, што корыстнік быстра пішчыць у поле:

A
AB
ABC
ABCD
ABCDE

Сінхронная відраскліванне дорогих частака інтэрфейсу пасля кожнага натыкання на клавішу, без можлівасці надаць прыоритэт, змусіла бы складныя экраны працаваць медленна. Функцыя планування дазволяе React аб’еднаваць апдэйты, якія прыходзяць разам, і, за дапомогою функцый конкурэнтнай працы, такіх як переходы, ставіцца да трыбуны няхіткіх апдэйтаў, напрыклад самага вводу, інакш як да менш няхіткіх, напрыклад спіса фільтраваных рэзультатаў. Гэтая можлівасць є важлівай часткай таго, чаму архітэктура React перайшла да планаваных, а не негараздовых апдэйтаў.

Шаг 3: фаза відрасклівання зноў запускае компонент

Калі React вырашае, што настала час обрабаваць апэліваючыя апдэйты, ён пачынае новае відраскліванне. Відраскліванне проста значыць зноў вызыванне функцыі компонента:

function Counter() {
    const [count, setCount] = useState(0);

    return <h1>{count}</h1>;
}

Важным моментам ёсць тое, што робіць useState пад час гэтага вызову. Перш чым вернуць значэнне, ён обрабоўвае запісаныя змены па адносу да паказанага раней стану. Пачынаючы з:

Previous State = 0

React праходзіць па чергавай:

Queue:
[ +1 ]Process QueueResult:
1

і значэнне, якое useState вернае, тепер ёсць:

count = 1

якое компонент выкарыстоўвае пад час гэтага адрасавання. Самэ таму новае значэнне становіцца видным толькі пад час наступнага адрасавання: яго вырахоўваюць там, а не у момент вызову функцыі-змінніка.

Шаг 4: ствараецца новая дрэва элементаў

Выкананне компонента вяртае чыстае дрэва элементаў React. Паралельна з паказаннем раней, яно выглядае так:

Previous Render
<h1>0</h1>

New Render
<h1>1</h1>

DOM браузера яшчэ не быў зменены. Усё, што ў цей момент мае React, — это апошнія даны пра паказаную інтэрфейсную схему.

Шаг 5: процес парапорання знаходзіць разніцу

Далей React парабяляе паканяны рэзультат:

Old Tree

з сяродзінным:

New Tree

шукаючы, што на самай працо змянілася. У гэтым прыкладзе, да пачатку:

Before:
<h1>0</h1>

і пасля:

After:
<h1>1</h1>

Толькі тэкст унутры загаловка разны, таму гэта ўсё, што змінюецца і што фіксуе React. Гэта парабяленне і є тым, што называецца процэсам сапраўджэння. Чырвоныя деталі таго, як React выбірае, што залишыць, а шта заменіць, можна пазнакоміцца ў стацыі пра стварэнне ментальнага моделю для сапраўджэння, стану і хуків у React.

Шаг 6: фаза падтверджэння змянавае DOM

Калі спіс змян вядом, React пераходзіць да фазы падтверджэння і прыкладзае ўсія змяны да рэальнага DOM:

DOM Before
<h1>0</h1>

DOM After
<h1>1</h1>

Толькі зараз корыстнік бачыць новую цэчу. Гэта той момент, які большасць людзей уявляе, калі вызываюць функцыю-зменяйшык, але на самай працэ ён яўляецца последнім з кальколька этапаў.

Чаму React не застосоўвае апдэйты негайна

Разглянем функцыю-обрабоўчык, якая адразу ж апдэйтуе кальколька элементаў стану:

setCount(c => c + 1);
setLoading(false);
setUser(data);

Якбы кожны вызыв спрычыняў свой сабстоятны рэндер, то вы былі б атрымалі:

Render 1
Render 2
Render 3

Это значыць тры рэндеры, з якіх два ўтрачаныя, і можліва проміжныя экраны, якія паказваюць неадэкватныя комбінацыі стану. У замен React групавае апдэйты. Вызывы стаюць у чергу:

Update
Update
Update

і пасля таго ўсе яны обрабоўваюцца разам:

      |
      ▼Single Render

Агрэгаванне запобегае зайвым перрадарам і гарантуе, што корыстнік завжды бачыць аднообразны заканчальны стан. Гэта галоўная прычына, чаму React воліе планаванне замест на выконванне кожнай змены негараздо. React таксама можа абоўсюды праскочыць певныя дзеянні: якщо змена дае значэнне, ідентычна ныякшаму, як гэта выявляе Object.is, React можа не перрадараваць дзецей компонента.

Як гэта выглядае на практыцы

Возьмімо поле пошуку, дзе кожны натыск клавішы зменяе тры элементы стану:

query
results
loading

Ўсвядома можна прыпусціць, што кожны з гэтых сетараў вызывае аднальны перрадар. Але аналіз такога компонента зазвычай паказвае іншае: змены, якія выкарыстоўваюцца разам у аднам заходзе, агрэгуюцца, таму компонент перрадарае менш часта, ніж можна было б вырачыць на простам адгледзе некоду. Задача React — не толькі застосавляць змены, але і робіць гэта эфектыва.

Калі калькольныя часткі маюць незавершаныя апдэйты

Апдэйты не обмежваюцца адной часткай. Разглянем такое дрэва:

App
 ├── Header
 ├── Sidebar
 └── Dashboard

Якщо калькольныя апдэйты падаюць у разныя часткі гэтаго дрэва, React не павінен слепа перзбудаваць усё. Дрэва Fiber дазволяюць React стежыць за:

  • калькольнымі часткамі, якія маюць незавершаную роботу
  • там, у дрэве, дзе кожны апдэйт выйшаў
  • калькольнымі паддрэвамі, якія трэба адвідаваць, і тымі, якія можна праскочыць

Гэта і ўзрабатвае тое, што React можа быць набліжэй выбірачным, чым падход «перзрэндарваць усю аплікацыю пасля кожных змян». Заўважкайце, што частка, якая перзрэндарваецца, за замовчаннем яшчэ перзрэндарвае сваіх дзецей; мемоізацыя дазволяе праскочыць незменаваныя паддрэвы.

Паўтарны адгляд застарэлага console.log

Вернімся да фрагмента з самага пачатку:

setCount(count + 1);
console.log(count);

Кансоль выведае:

0

Таму што код яшчэ выкананыць у момент часовога адраджавання. Калека яшчэ не обработана, наступна перадрукаванне ўжо не адбылася, і апдэйт чакае своей разы. Более тачны спосаб прыкладзіць гэта:

Current Render
count = 0

Request Update
Future Render
count = 1

Якщо вам трэба новая значэнне негайна, вырахавайце яе ў локальнай зменной і выкорыстоўвайце яе, або прачытайце яе пад час наступнай перадрукаванне або ў эфекте, які залежыць ад яе. Калі паглядаць так, гэтая поведзенне зовсім не ўжасна.

Як соедынуюцца элементы

Усе разам этыя этапы утвараюць адну ланцоўку:

  • Стан зберагаецца разам з дадзеннямі Hook компонента.
  • Дадзеныя Hook прыўязуюцца да Fiber компонента.
  • Выклік сетара створыць апдэйт.
  • Апдэйты дадаюцца да калекі Hook.
  • Раскладчык выбирае момент, калі ўжо можна іх обработаць.
  • Этап перадрукавання знову выклікае компонент і прыменяе калеку.
  • Пры супрацоўкі парабягаецца новыя дрэвы элементаў з старымі.
  • На этапе зафіксавання змяны прымаюцца ў DOM.
  • Канцэпцыі, якія часта выкладваюцца окаласці, на самай працэ ёсць вырастленымі крокамі ў адной паўтароўкай обробкі. У адной фразе: вызов setter-а залишае стан такім, які ён є, і замест таго просіць аб запланаванай апдэйтаванні, якая React прымае праз час у наступным рэндары, апрацоўвае разлікі з пярэднім выходам і зафіксавае змяны ў DOM толькі там, дзе ёсць разлікі.

    Галоўныя выводы

    • Setter ніколі не мутуюць стан на месцы; ён додае апдэйт у чергу, каб React могаў яго апрацаваць пазней.
    • Апдэйты кансалююцца па кожнаму Hook, таму калькі з іх можна апрацаваць у аднам рэндары.
    • Простыя значэнні заменяюць стан; функцыі-апдэйтары вырахоўваюць наступны стан на адной з найсвежэйшых, чым утрымліваецца вялікі стан.
    • React плануе выконанне задач усьмоўна, а не выконвае ўраджэнне з кожным вызывам, і самэ гэта дазволяе атрымляць пакетаванне і прыорітэзацыю.
    • Ураджэнне і апдэйт DOM — гэта разныя этапы: на этапе ураджэння ствараецца опис, а на этапе практыкавання змінюецца прыстрой перагляду.
    • Калека апдэйтаў знаходзіцца разам з станам Hook у Fiber-компоненте.

    Расследжванне setState як запиту, а не як каманды, — гэта незначны змянення формуліравання, але яно мае вялікія наследкі. Самэ гэта дазволяе атрымляць пакетаванне, плануванне, супрацоўку з розніцай станаў і адночаснае ураджэнне. Наступныя пытанні — гэта як React выбірае, чы робяць апдэйты пакетам даўае аднае ураджэнне чы калькі, і самэ гэта рашаецца за дапамогою аўтаматычнага пакетавання, якое было адкрытыя ў React 18.