Atskleiskite magiją, slypinčią už React našumo. Šis išsamus vadovas paaiškina „Reconciliation“ algoritmą, Virtualaus DOM palyginimą ir pagrindines optimizavimo strategijas.
React slaptasis ingredientas: išsami „Reconciliation“ algoritmo ir Virtualaus DOM palyginimo analizė
Šiuolaikinio interneto programavimo pasaulyje React tapo dominuojančia jėga, kuriant dinamiškas ir interaktyvias vartotojo sąsajas. Jo populiarumą lemia ne tik komponentais pagrįsta architektūra, bet ir išskirtinis našumas. Bet kas daro React tokį greitą? Atsakymas nėra magija; tai yra genialus inžinerinis sprendimas, žinomas kaip „Reconciliation“ algoritmas.
Daugeliui programuotojų vidinis React veikimas yra juoda dėžė. Mes rašome komponentus, valdome būseną ir stebime, kaip vartotojo sąsaja nepriekaištingai atsinaujina. Tačiau supratimas apie mechanizmus, slypinčius už šio sklandaus proceso, ypač apie Virtualų DOM ir jo palyginimo algoritmą, skiria gerą React programuotoją nuo puikaus. Šios giluminės žinios suteikia galimybę kurti itin optimizuotas programas, derinti našumo problemas ir iš tikrųjų įvaldyti biblioteką.
Šis išsamus vadovas atskleis React pagrindinio atvaizdavimo proceso paslaptis. Išnagrinėsime, kodėl tiesioginis DOM manipuliavimas yra brangus, kaip Virtualus DOM siūlo elegantišką sprendimą ir kaip „Reconciliation“ algoritmas efektyviai atnaujina jūsų vartotojo sąsają. Taip pat pasigilinsime į evoliuciją nuo pradinio „Stack Reconciler“ iki šiuolaikinės „Fiber“ architektūros ir baigsime veiksmingomis strategijomis, kurias galite pritaikyti jau šiandien, norėdami optimizuoti savo programas.
Pagrindinė problema: kodėl tiesioginis DOM manipuliavimas yra neefektyvus
Norėdami įvertinti React sprendimą, pirmiausia turime suprasti problemą, kurią jis sprendžia. Dokumento objektų modelis (DOM) yra naršyklės API, skirta HTML dokumentams vaizduoti ir su jais sąveikauti. Jis yra struktūrizuotas kaip objektų medis, kur kiekvienas mazgas atspindi dokumento dalį (pavyzdžiui, elementą, tekstą ar atributą).
Kai norite pakeisti tai, kas rodoma ekrane, jūs manipuliuojate šiuo DOM medžiu. Pavyzdžiui, norėdami pridėti naują sąrašo elementą, sukuriate naują `
- ` mazgo. Nors tai atrodo paprasta, DOM operacijos yra skaičiavimo požiūriu brangios. Štai kodėl:
- Išdėstymas ir perskaičiavimas (Reflow): Kiekvieną kartą, kai keičiate elemento geometriją (pavyzdžiui, jo plotį, aukštį ar poziciją), naršyklė turi iš naujo apskaičiuoti visų paveiktų elementų pozicijas ir matmenis. Šis procesas vadinamas „reflow“ arba „layout“ ir gali kaskadiškai paveikti visą dokumentą, sunaudodamas daug apdorojimo galios.
- Perpiešimas (Repainting): Po „reflow“ naršyklė turi perpiešti ekrano pikselius atnaujintiems elementams. Tai vadinama „repainting“ arba „rasterizing“. Pakeitus kažką paprasto, pavyzdžiui, fono spalvą, gali būti suaktyvintas tik perpiešimas, tačiau išdėstymo pakeitimas visada suaktyvins perpiešimą.
- Sinchroniškas ir blokuojantis: DOM operacijos yra sinchroniškos. Kai jūsų JavaScript kodas modifikuoja DOM, naršyklė dažnai turi sustabdyti kitas užduotis, įskaitant reagavimą į vartotojo įvestį, kad atliktų „reflow“ ir perpiešimą, o tai gali sukelti lėtą arba užstrigusią vartotojo sąsają.
- Pradinis atvaizdavimas: Kai jūsų programa pirmą kartą įkeliama, React sukuria visą Virtualaus DOM medį jūsų vartotojo sąsajai ir naudoja jį, kad sugeneruotų pradinį realų DOM.
- Būsenos atnaujinimas: Kai programos būsena pasikeičia (pavyzdžiui, vartotojas paspaudžia mygtuką), React sukuria naują Virtualaus DOM medį, kuris atspindi naują būseną.
- Palyginimas (Diffing): Dabar React atmintyje turi du Virtualaus DOM medžius: senąjį (prieš būsenos pasikeitimą) ir naująjį. Tada jis paleidžia savo „diffing“ (palyginimo) algoritmą, kad palygintų šiuos du medžius ir nustatytų tikslius skirtumus.
- Grupavimas ir atnaujinimas: React apskaičiuoja efektyviausią ir minimalų operacijų rinkinį, reikalingą atnaujinti realų DOM, kad jis atitiktų naująjį Virtualų DOM. Šios operacijos yra sugrupuojamos ir pritaikomos realiam DOM vienoje, optimizuotoje sekoje.
- Jis sunaikina visą seną medį, atjungdamas visus senus komponentus ir sunaikindamas jų būseną.
- Jis sukuria visiškai naują medį nuo nulio, remdamasis nauju elemento tipu.
- Elementas B
- Elementas C
- Elementas A
- Elementas B
- Elementas C
- Jis lygina seną elementą su indeksu 0 ('Elementas B') su nauju elementu su indeksu 0 ('Elementas A'). Jie skiriasi, todėl jis modifikuoja pirmąjį elementą.
- Jis lygina seną elementą su indeksu 1 ('Elementas C') su nauju elementu su indeksu 1 ('Elementas B'). Jie skiriasi, todėl jis modifikuoja antrąjį elementą.
- Jis mato, kad yra naujas elementas su indeksu 2 ('Elementas C'), ir jį įterpia.
- Elementas B
- Elementas C
- Elementas A
- Elementas B
- Elementas C
- React peržiūri naujo sąrašo vaikinius elementus ir randa elementus su raktais 'b' ir 'c'.
- Jis žino, kad elementai su raktais 'b' ir 'c' jau egzistuoja sename sąraše, todėl juos tiesiog perkelia.
- Jis mato, kad yra naujas elementas su raktu 'a', kurio anksčiau nebuvo, todėl jį sukuria ir įterpia.
- ... )`) yra anti-šablonas, jei sąrašas kada nors gali būti pertvarkytas, filtruojamas ar iš jo gali būti pridedami/šalinami elementai viduryje, nes tai sukelia tas pačias problemas, kaip ir neturint rakto. Geriausi raktai yra unikalūs identifikatoriai iš jūsų duomenų, pavyzdžiui, duomenų bazės ID.
- Inkrementinis atvaizdavimas: Jis gali padalinti atvaizdavimo darbą į mažus gabalėlius ir paskirstyti jį per kelis kadrus.
- Prioritetų nustatymas: Jis gali priskirti skirtingus prioritetų lygius skirtingų tipų atnaujinimams. Pavyzdžiui, vartotojo rašymas įvesties laukelyje turi aukštesnį prioritetą nei duomenų gavimas fone.
- Galimybė sustabdyti ir nutraukti: Jis gali sustabdyti darbą su žemo prioriteto atnaujinimu, kad galėtų apdoroti aukšto prioriteto atnaujinimą, ir netgi gali nutraukti ar pakartotinai panaudoti darbą, kuris nebėra reikalingas.
- Atvaizdavimo / Suderinimo fazė (Asinchroninė): Šioje fazėje React apdoroja „fiber“ mazgus, kad sukurtų „work-in-progress“ (vykdomo darbo) medį. Jis iškviečia komponentų `render` metodus ir paleidžia palyginimo algoritmą, kad nustatytų, kokius pakeitimus reikia atlikti DOM. Svarbiausia, kad ši fazė yra pertraukiama. React gali sustabdyti šį darbą, kad atliktų kažką svarbesnio, ir vėliau jį tęsti. Kadangi ji gali būti pertraukta, React netaiko jokių faktinių DOM pakeitimų šioje fazėje, kad išvengtų nenuoseklios vartotojo sąsajos būsenos.
- Įvykdymo fazė (Sinchroninė): Kai „work-in-progress“ medis yra baigtas, React pereina į įvykdymo fazę. Jis paima apskaičiuotus pakeitimus ir pritaiko juos realiam DOM. Ši fazė yra sinchroninė ir negali būti pertraukta. Tai užtikrina, kad vartotojas visada matytų nuoseklią vartotojo sąsają. Gyvavimo ciklo metodai, tokie kaip `componentDidMount` ir `componentDidUpdate`, taip pat `useLayoutEffect` ir `useEffect` „hooks“, yra vykdomi šioje fazėje.
- `React.memo()`: Aukštesnės eilės komponentas funkcijų komponentams. Jis atlieka paviršutinišką komponento savybių (`props`) palyginimą. Jei savybės nepasikeitė, React praleis komponento perpiešimą ir pakartotinai panaudos paskutinį atvaizduotą rezultatą.
- `useCallback()`: Komponento viduje apibrėžtos funkcijos yra sukuriamos iš naujo kiekvieno atvaizdavimo metu. Jei šias funkcijas perduodate kaip savybes (`props`) į vaikinį komponentą, apgaubtą `React.memo`, vaikinis komponentas bus perpieštas, nes funkcijos savybė techniškai yra nauja funkcija kiekvieną kartą. `useCallback` memoizuoja pačią funkciją, užtikrindama, kad ji būtų sukurta iš naujo tik pasikeitus jos priklausomybėms.
- `useMemo()`: Panašus į `useCallback`, bet skirtas reikšmėms. Jis memoizuoja brangaus skaičiavimo rezultatą. Skaičiavimas yra paleidžiamas iš naujo tik pasikeitus vienai iš jo priklausomybių. Tai naudinga norint išvengti brangių skaičiavimų kiekvieno atvaizdavimo metu ir išlaikyti stabilias objektų/masyvų nuorodas, perduodamas kaip savybes.
Įsivaizduokite sudėtingą programą su tūkstančiais mazgų. Jei atnaujintumėte būseną ir naiviai perpieštumėte visą vartotojo sąsają tiesiogiai manipuliuodami DOM, priverstumėte naršyklę atlikti brangių „reflow“ ir perpiešimų kaskadą, o tai sukeltų baisią vartotojo patirtį.
Sprendimas: Virtualus DOM (VDOM)
React kūrėjai atpažino tiesioginio DOM manipuliavimo našumo problemą. Jų sprendimas buvo įdiegti abstrakcijos lygmenį: Virtualų DOM.
Kas yra Virtualus DOM?
Virtualus DOM yra lengvas, atmintyje esantis realaus DOM atvaizdas. Iš esmės tai yra paprastas JavaScript objektas, aprašantis vartotojo sąsają. VDOM objektas turi savybes, kurios atspindi realaus DOM elemento atributus. Pavyzdžiui, paprastas `
{ type: 'div', props: { className: 'container', children: 'Hello World' } }
Kadangi tai yra tik JavaScript objektai, juos kurti ir manipuliuoti yra neįtikėtinai greita. Tai nereikalauja jokios sąveikos su naršyklės API, todėl nevyksta jokie „reflow“ ar perpiešimai.
Kaip veikia Virtualus DOM?
VDOM leidžia taikyti deklaratyvų požiūrį į vartotojo sąsajos kūrimą. Užuot nurodę naršyklei, kaip pakeisti DOM žingsnis po žingsnio (imperatyviai), jūs tiesiog deklaruojate, kaip vartotojo sąsaja turėtų atrodyti esant tam tikrai būsenai (deklaratyviai). React pasirūpina likusiu darbu.
Procesas atrodo taip:
Grupuodamas atnaujinimus, React sumažina tiesioginę sąveiką su lėtu DOM, žymiai pagerindamas našumą. Šio efektyvumo esmė slypi „diffing“ žingsnyje, kuris formaliai žinomas kaip „Reconciliation“ algoritmas.
React šerdis: „Reconciliation“ algoritmas
„Reconciliation“ yra procesas, per kurį React atnaujina DOM, kad jis atitiktų naujausią komponentų medį. Algoritmas, atliekantis šį palyginimą, yra tai, ką vadiname „diffing“ (palyginimo) algoritmu.
Teoriškai, minimalaus transformacijų skaičiaus radimas, norint paversti vieną medį kitu, yra labai sudėtinga problema, kurios algoritmo sudėtingumas yra O(n³), kur n yra mazgų skaičius medyje. Tai būtų per lėta realaus pasaulio programoms. Siekdama tai išspręsti, React komanda padarė keletą genialių pastebėjimų apie tai, kaip paprastai veikia interneto programos, ir įdiegė euristinį algoritmą, kuris yra daug greitesnis – veikiantis O(n) laiku.
Heuristikos: kaip padaryti palyginimą greitą ir nuspėjamą
React palyginimo algoritmas pagrįstas dviem pagrindinėmis prielaidomis arba heuristikomis:
Heuristika 1: Skirtingų tipų elementai sukuria skirtingus medžius
Tai pirmoji ir paprasčiausia taisyklė. Lygindamas du VDOM mazgus, React pirmiausia žiūri į jų tipą. Jei šakninių elementų tipai skiriasi, React daro prielaidą, kad programuotojas nenori bandyti vieno konvertuoti į kitą. Vietoj to, jis imasi drastiškesnio, bet nuspėjamo požiūrio:
Pavyzdžiui, apsvarstykite šį pakeitimą:
Prieš: <div><Counter /></div>
Po: <span><Counter /></span>
Nors vidinis `Counter` komponentas yra tas pats, React mato, kad šakninis elementas pasikeitė iš `div` į `span`. Jis visiškai atjungs seną `div` ir jame esantį `Counter` egzempliorių (prarandant jo būseną), o tada prijungs naują `span` ir visiškai naują `Counter` egzempliorių.
Pagrindinė išvada: Venkite keisti komponento submedžio šakninio elemento tipo, jei norite išsaugoti jo būseną arba išvengti visiško to submedžio perpiešimo.
Heuristika 2: Programuotojai gali nurodyti stabilius elementus naudodami `key` savybę
Tai yra bene svarbiausia heuristika, kurią programuotojai turi suprasti ir teisingai taikyti. Kai React lygina vaikinių elementų sąrašą, jo numatytasis elgesys yra iteruoti per abu vaikinių sąrašus vienu metu ir generuoti mutaciją ten, kur yra skirtumas.
Problema su indeksu pagrįstu palyginimu
Įsivaizduokime, kad turime elementų sąrašą ir pridedame naują elementą į sąrašo pradžią, nenaudodami raktų (`key`).
Pradinis sąrašas:
Atnaujintas sąrašas (pridėtas 'Elementas A' pradžioje):
Be raktų, React atlieka paprastą, indeksu pagrįstą palyginimą:
Tai yra labai neefektyvu. React atliko dvi nereikalingas mutacijas ir vieną įterpimą, kai reikėjo tik vieno įterpimo pradžioje. Jei šie sąrašo elementai būtų sudėtingi komponentai su savo būsena, tai galėtų sukelti rimtų našumo problemų ir klaidų, nes būsena galėtų susimaišyti tarp komponentų.
`key` savybės galia
`key` savybė suteikia sprendimą. Tai speciali eilutės tipo savybė, kurią turite įtraukti kurdami elementų sąrašus. Raktai suteikia React stabilią tapatybę kiekvienam elementui.
Grįžkime prie to paties pavyzdžio, bet šį kartą su stabiliais, unikaliais raktais:
Pradinis sąrašas:
Atnaujintas sąrašas:
Dabar React palyginimo procesas yra daug protingesnis:
Tai yra daug efektyviau. React teisingai nustato, kad reikia atlikti tik vieną įterpimą. Komponentai, susieti su raktais 'b' ir 'c', yra išsaugomi, išlaikant jų vidinę būseną.
Kritinė taisyklė raktams: Raktai turi būti stabilūs, nuspėjami ir unikalūs tarp savo kaimynų. Masyvo indekso naudojimas kaip rakto (`items.map((item, index) =>
Evoliucija: nuo „Stack“ iki „Fiber“ architektūros
Aukščiau aprašytas „reconciliation“ algoritmas daugelį metų buvo React pagrindas. Tačiau jis turėjo vieną didelį apribojimą: jis buvo sinchroniškas ir blokuojantis. Šis pradinis įgyvendinimas dabar vadinamas „Stack Reconciler“.
Senasis būdas: „Stack Reconciler“
„Stack Reconciler“ atveju, kai būsenos atnaujinimas sukeldavo perpiešimą, React rekursyviai pereidavo visą komponentų medį, apskaičiuodavo pakeitimus ir pritaikydavo juos DOM – viską vienoje, nepertraukiamoje sekoje. Mažiems atnaujinimams tai buvo gerai. Bet dideliems komponentų medžiams šis procesas galėjo užtrukti daug laiko (pvz., daugiau nei 16ms), blokuodamas pagrindinę naršyklės giją. Tai sukeldavo nereaguojančią vartotojo sąsają, prarastus kadrus, trūkčiojančias animacijas ir prastą vartotojo patirtį.
Pristatome React Fiber (React 16+)
Siekdama išspręsti šią problemą, React komanda ėmėsi kelerius metus trukusio projekto, siekdama visiškai perrašyti pagrindinį „reconciliation“ algoritmą. Rezultatas, išleistas su React 16, vadinamas React Fiber.
„Fiber“ architektūra buvo sukurta nuo pat pradžių, kad būtų įgalintas konkurentiškumas – galimybė React dirbti su keliomis užduotimis vienu metu ir persijungti tarp jų pagal prioritetą.
„Fiber“ yra paprastas JavaScript objektas, kuris atspindi darbo vienetą. Jame saugoma informacija apie komponentą, jo įvestį (props) ir išvestį (children). Užuot naudojus rekursyvų perėjimą, kurio negalima nutraukti, React dabar apdoroja susietą „fiber“ mazgų sąrašą, po vieną.
Ši nauja architektūra atvėrė keletą pagrindinių galimybių:
Dvi „Fiber“ fazės
Pagal „Fiber“, atvaizdavimo procesas yra padalintas į dvi atskiras fazes:
„Fiber“ architektūra yra pagrindas daugeliui šiuolaikinių React funkcijų, įskaitant `Suspense`, konkurentinį atvaizdavimą, `useTransition` ir `useDeferredValue`, kurios visos padeda programuotojams kurti labiau reaguojančias ir sklandesnes vartotojo sąsajas.
Praktinės optimizavimo strategijos programuotojams
React „reconciliation“ proceso supratimas suteikia jums galią rašyti našesnį kodą. Štai keletas praktinių strategijų:
1. Visada naudokite stabilius ir unikalius raktus sąrašams
To negalima pabrėžti per daug. Tai yra pati svarbiausia sąrašų optimizacija. Naudokite unikalų ID iš savo duomenų (pvz., `product.id`). Venkite naudoti masyvo indeksus, nebent sąrašas yra visiškai statiškas ir niekada nepasikeis.
2. Venkite nereikalingų perpiešimų
Komponentas perpiešiamas, jei pasikeičia jo būsena arba perpiešiamas jo tėvinis komponentas. Kartais komponentas perpiešiamas net tada, kai jo išvestis būtų identiška. Galite to išvengti naudodami:
3. Išmani komponentų kompozicija
Tai, kaip struktūrizuojate savo komponentus, gali turėti didelės įtakos našumui. Jei dalis jūsų komponento būsenos dažnai atnaujinama, pabandykite ją atskirti nuo tų dalių, kurios nesikeičia.
Pavyzdžiui, užuot turėję vieną didelį komponentą, kuriame dažnai kintantis įvesties laukas sukelia viso komponento perpiešimą, perkelkite tą būseną į atskirą, mažesnį komponentą. Tokiu būdu, kai vartotojas rašo, perpiešiamas tik mažas komponentas.
4. Virtualizuokite ilgus sąrašus
Jei reikia atvaizduoti sąrašus su šimtais ar tūkstančiais elementų, net ir su tinkamais raktais, visų jų atvaizdavimas vienu metu gali būti lėtas ir sunaudoti daug atminties. Sprendimas yra virtualizacija arba lango principas (windowing). Ši technika apima tik mažo elementų poaibio, kuris šiuo metu matomas peržiūros srityje, atvaizdavimą. Kai vartotojas slenka, seni elementai yra atjungiami, o nauji prijungiami. Bibliotekos, tokios kaip `react-window` ir `react-virtualized`, suteikia galingus ir lengvai naudojamus komponentus šiam modeliui įgyvendinti.
Išvada
React našumas nėra atsitiktinumas; tai yra sąmoningos ir sudėtingos architektūros, orientuotos į Virtualų DOM ir efektyvų „Reconciliation“ algoritmą, rezultatas. Abstrahuodamas tiesioginį DOM manipuliavimą, React gali grupuoti ir optimizuoti atnaujinimus taip, kaip būtų neįtikėtinai sudėtinga valdyti rankiniu būdu.
Mes, programuotojai, esame svarbi šio proceso dalis. Suprasdami palyginimo algoritmo heuristikas – tinkamai naudodami raktus, memoizuodami komponentus ir reikšmes bei apgalvotai struktūrizuodami savo programas – galime dirbti su React „reconciler“, o ne prieš jį. Evoliucija į „Fiber“ architektūrą dar labiau išplėtė galimybių ribas, įgalindama naujos kartos sklandžias ir reaguojančias vartotojo sąsajas.
Kitą kartą, kai pamatysite, kad jūsų vartotojo sąsaja akimirksniu atsinaujina po būsenos pakeitimo, akimirkai įvertinkite elegantišką Virtualaus DOM, palyginimo algoritmo ir įvykdymo fazės šokį, vykstantį po variklio gaubtu. Šis supratimas yra jūsų raktas į greitesnių, efektyvesnių ir tvirtesnių React programų kūrimą pasaulinei auditorijai.