Responsive Svelte (utforsker Sveltes-reaktivitet)

Svelte er virkelig en ny og revolusjonerende tilnærming til webutvikling! Den gjør en fantastisk jobb med å automatisere appens reaktivitet.

Denne artikkelen fokuserer på å avdekke noen "bak scenen" detalj om Sveltes reaktivitet . Vi vil gjøre dette fra et "observatørperspektiv" - ved å visualisere Sveltes reaktivitet førstehånds (rett i applikasjonen vår) ! Det er litt av et mysterium bak noen av disse konseptene. Vi skal dykke ned i noen mindre kjente detaljer (som du kanskje ikke har vurdert) ! Håpet er at du vil komme unna med mer innsikt og bli bedre informert om hvordan du bruker dette fantastiske produktet!

Dette er ikke en Svelte-opplæring . For det meste bør enhver utvikler være i stand til å forstå konseptene som er diskutert her. Ideelt sett bør du kunne det grunnleggende om Svelte. Selv om du ikke trenger å være ekspert, vil vi ikke det bruke tid på å forklare rudimentære Svelte-konstruksjoner.

En stor takk til Mark Volkmann for hans anmeldelse av min innsats. Jeg har akkurat startet min Svelte-reise, så Marks innsikt var uvurderlig! Det kan være lurt å sjekke ut hans nye bok:Svelte and Sapper in Action.

Med et blikk

  • TL;DR
  • Videopresentasjon
  • Svelte Reactivity
  • Reaktive utløsere
    • Primitive typer
    • Objekttyper
    • Staleness-sammendrag
  • Utforsk appreaktivitet
    • Diagnostiske loggingsonder
    • Avansert diagnostikk
  • Demo-app
  • Inspeksjon av appreaktivitet
    • Loggingprober
    • Avanserte sonder
  • Gjengivelsesanalyse på nytt
    • Utreferanseobjekter
    • Sveltes re-render-optimalisering
    • Svelte Compiler Output
    • Sveltes refleksivitet er veldig effektivt
  • Tweaks for appreaktivitet
    • Finnere avhengighetsstyring
    • Forhåndsavgjør variasjoner
    • Optimaliseringsadvarsler
  • Ekstra kredittøvelse
  • Hvem er denne fyren?
  • Sammendrag

TL;DR

Som hovedregel vil jeg anbefale å lese artikkelen i sin helhet. Med det sagt, hvis du ønsker å "skjære til" , jeg har oppsummert funnene mine på slutten ... hopp gjerne videre til sammendraget!

Videopresentasjon

Hvis du er visuelt tilbøyelig , Jeg holdt følgende presentasjon til Svelte-toppmøtet høsten 2021:

Fullstendig pensum og ressurslenker

Svelte-reaktivitet

Siden jeg er ny i Svelte, er jeg helt fascinert av det faktum at "reaktivitet er rett og slett bakt inn i appen min" ! Dette er sååååååååååååååååååååååååååååååååååååååååå mye !

I rammeverk som React må appen din utløse reaktivitet (med ting som setState() ) , og det store fokuspunktet er:"Når gjengir komponenten min" ?

Med Svelte skjer reaktivitet automatisk, og den er mye finere kornet enn komponentnivået. I Svelte kan individuelle utdrag av enhver komponent dynamisk regenereres når som helst !

Svelte administrerer omhyggelig hver kodebit, overvåker og kjører dem på nytt etter behov, basert på endringer i deres avhengige tilstand. Her ligger skjønnheten til Svelte:den oppnår alt dette automatisk gjennom sin kompilatorfilosofi ... konverterer vår deklarative komponentbaserte kode til JavaScript som trinnvis manipulerer DOM direkte! Svelte eliminerer både standardkode og er virkelig reaktiv rett ut av esken, uten å bruke et oppblåst rammeverk for kjøretid i minnet. Hvor kul er det?

hva (du spør kanskje) er en kodebit ? Dette er faktisk min term . I denne artikkelen brukes begrepet "snippet" refererer til ethvert JavaScript-uttrykk som Svelte reaktivt administrerer og kjører på nytt på riktig tidspunkt (dvs. når avhengig tilstand endres) . Til syvende og sist brukes utdrag for å gi dynamikken til HTML-koden vår (dvs. det er reaktivitet) .

Utdrag kan finnes to steder :

  1. kodebiter :
    kodebiter finnes i komponentens JavaScript-kode (innen <script>). tag) , og merket med Sveltes $: merkelapp. Dette omtales som Reaktive erklæringer og Reaktive erklæringer .

    $: {name, phone} = user;
    

    Generelt sett kodebiter er vanligvis lette, ved at de resulterer i endringer i JavaScript-tilstandsvariabler. Når det er sagt, er den eneste grunnen til disse tilstandsendringene å referere til i html-koden vår (enten direkte eller indirekte) .

    I eksemplet ovenfor vil kodebiten kjøres på nytt hver gang user objektendringer (kodebitens avhengighet) , tilordne name på nytt og phone variabler.

  2. html-snippets :
    html-snippets finnes i komponentens html-markering, avgrenset med krøllete klammeparenteser:{...} . Dette blir ofte referert til som interpolering .

    <p>Hello {name}</p>
    <p>May we call you at {phone}</p>
    

    html-snippets er vanligvis mer tyngre, ved at de resulterer i endringer i html DOM ! Men hei ... det er hele grunnen til at vi er her ... dvs. kjernepunktet i vår reaktivitet !

    I eksemplet ovenfor vil den første kodebiten gjenskape sin html når name endringer, og den andre når phone endringer.

Terminologi :snippet, code-snippet og html-snippet

Reaktive utløsere

Så hvis du ser litt nærmere på dette emnet, spør du kanskje:Hvordan bestemmer Svelte når den skal utløse omkjøringen av utdragene våre?

Det korte svaret er at Svelte overvåker den avhengige tilstanden det refereres til i hver kodebit, og utløser en ny utførelse hver gang den tilstanden endres.

Oppfølgingsspørsmålet er:Hvordan fastslår Svelte at statens referanser har endret seg?

Svelte-dokumentene snakker om «Oppgaver er «reaktive»» og «Sveltes reaktivitet utløses av tildeling» . Det de sier er at Svelte utløser reaktivitet gjennom oppgavesemantikk (gjenkjenne oppgave i ulike former) .

Dette gjelder for den lokale tilstanden til en komponent. Svelte-kompilatoren vil gjenkjenne oppgaven (i dens forskjellige former) , og merk den tilordnede variabelen som endret (dvs. "foreldet") .

Jeg har imidlertid oppdaget at det er en stor forskjell på om tildelingsmålet er et primitivt eller et objekt (inkludert matriser) .

Primitive typer

For primitive typer (strenger, tall, booleaner osv.) , oppstår reaktivitet bare når verdien har endret seg. Med andre ord, den inneholder også JavaScript-identitetssemantikk (dvs. priorState === nextState ).

myNum = (x + y) / 2 vil bare bli merket som "foreldet" når verdien faktisk endres. Hvis den tidligere verdien var 10 og beregningen resulterte i 10, vil ingen reaktivitet forekomme.

Dette er absolutt hva man kan forvente, og jeg antar at det er ganske åpenbart. Svelte-dokumentene nevner imidlertid ikke dette (så vidt jeg kan se) . Den eneste grunnen til at jeg dveler ved dette punktet er at det står i sterk kontrast til objektbasert reaktivitet.

Objekttyper

De fleste applikasjoner krever mer kompleks tilstand, vanligvis modellert i objekter (eller arrays) .

Som det viser seg, i Svelte, vil enhver teknikk der du endrer et objekt, merke hele objektet som "foreldet". Dette inkluderer lokale komponentobjekter, Svelte-objektlagre, komponentobjektegenskaper osv. Dette gjelder også når du muterer et objekt, og informerer Svelte om at objektet har endret seg (ved å tilordne det til seg selv) .

Dette betyr at granulariteten til endring som spores er mye bredere i omfang. Med andre ord vil hele objektet bli ansett som "foreldet" selv om bare én egenskap kan ha endret seg.

Innsikt :Reaktivitet er basert på endring i avhengig tilstand

Staleness Summary

Følgende tabell fremhever hva Svelte vil anse som "foreldet":

Given:
   let prim  = 1945; // a primitive
   let myObj = {     // an object
     foo: 1945,
     bar: 1945,
   };

Operation                      Marks this as "stale"
=============================  ======================================
prim++                         prim
prim = 1945                    prim (ONLY if prior value is NOT 1945)

myObj.foo++                    myObj (all content of myObj)
myObj = {...myObj, foo:1945}   myObj (all content of myObj)
myObj.foo = 1945               myObj (all content of myObj)
myObj = myObj                  myObj (all content of myObj)
incrementFooIndirectly(myObj)  NOTHING

Du kan se en demo av dette i min Reactive Triggers REPL. Dette visualiserer refleksive tellinger (uthevet i gult) , fra de forskjellige operasjonene (oppført ovenfor) . For å forstå hvordan denne REPL fungerer, må du vite hva en ReflectiveCounter er (et nytt verktøy for verktøybeltet ditt) . Dette er diskutert i avsnittet Avansert diagnostikk. Det kan være lurt å komme tilbake til denne REPL etter å ha lest neste avsnitt.

Utforsk appreaktivitet

Som den nysgjerrige karen jeg er, vil jeg se appens reaktivitet. Tross alt er jeg opprinnelig fra Missouri - "Show-Me"-staten !

Du kan kanskje si:"selvfølgelig kan du se de reaktive resultatene av produksjonsappen din, gjennom tilstanden den visualiserer" !

MEN NEI ... det er IKKE det jeg snakker om. Jeg vil definitivt bestemme når Svelte utløser kjøringen av utdragene mine! Med andre ord, jeg vil se Sveltes reaktivitet i aksjon !

Ved å gjøre dette vil dette:

  • hjelpe meg med Svelte-filosofien

  • gi meg innsikt i ulike Svelte-heuristikk (avhengighetsovervåking, reaktive triggere, DOM-oppdateringer osv.)

  • gi meg en bedre forståelse for "all denne reaksjonen som skjer (automatisk) rundt meg"

  • og vi oppdager kanskje noen detaljer som vi ikke hadde vurdert !

Selvfølgelig er dette noe som vil være begrenset til en kategori av "diagnostiske prober", og ikke en del av produksjonsappen vår.

Ved første øyekast virker dette som en "vanskelig oppgave", fordi Svelte har kontroll over dette (ikke oss) . Og Svelte Devtools gir heller ingen innsikt i dette (det er fokus på å vurdere tilstanden på et gitt tidspunkt) .

Diagnostiske loggingprober

Som det viser seg, kan vi bruke et vanlig "utviklertriks" for å logisk-ELLER et console.log()-prefiks til hver av kodebitene våre.

Tenk på dette:

Original:

<p>Hello {name}</p>
<p>May we call you at {phone}</p>

Med loggingsonder:

<p>Hello {console.log('Name section fired) || name}</p>
<p>May we call you at {console.log('Phone section fired) || phone}</p>

Vi har nå prefikset hvert produksjonsuttrykk med en console.log() som er logisk-ELLERT. Fordi console.log() returnerer ingenting (dvs. undefined som er falsk) , vil det påfølgende uttrykket kjøres ubetinget (som resulterer i den opprinnelige html-utdata).

Med andre ord vil dette generere den samme html (som vår originale produksjonskode) , men med tillegg av diagnostiske logger som sendes bare når kodebiten kjøres .

Som et eksempel, si vår phone tilstandsendringer ... vi vil se følgende sendes ut i loggene våre:

logger:

Phone section fired

Du kan se en live-demo av dette i Logging Probes-diskusjonen.

Det er viktig å bruke unike tekster i hver sonde , for å kunne korrelere hver loggoppføring til den tilhørende kodebiten.

Med tillegg av disse diagnostiske probene, vil loggene våre definitivt avsløre når Svelte kjører hver kodebit på nytt ... veldig bra!

Takeaway :Overvåk Svelte-snutt-anrop gjennom logisk-ELLER-prefiksuttrykk

Avansert diagnostikk

For de fleste applikasjoner vil disse enkle diagnostiske loggingprobene gi tilstrekkelig innsikt i appens refleksivitet.

Avhengig av hvor mange prober du trenger, kan det imidlertid bli kjedelig å korrelere disse loggene til de ulike seksjonene.

I disse tilfellene kan vi erstatte loggene med en enkel monitor, som eksponerer et reflekterende antall for hver seksjon, vises direkte på siden vår !

Her er verktøyet:

createReflectiveCounters.js

export default function createReflectiveCounter(logMsg) {
  // our base writable store
  // ... -1 accounts for our initial monitor reflection (bumping it to 0)
  const {subscribe, set, update} = writable(-1);

  // expose our newly created custom store
  return {
    subscribe,
    monitor(...monitorDependents) {
      update((count) => count + 1);  // increment our count
      logMsg && console.log(logMsg); // optionally log (when msg supplied)
      return ''; // prevent rendering `undefined` on page (when used in isolation)
                 // ... still `falsy` when logically-ORed
    },
    reset: () => set(0)
  };
}

Dette oppretter en ReflectiveCounter (en tilpasset butikk), egnet til å brukes til å overvåke Svelte-reflekterende teller.

I sin rudimentære form, en ReflectiveCounter er bare en enkel teller, men dens API er skreddersydd for å brukes som en reflekterende monitor.

monitor() metoden skal ha prefiks i en "Svelte påkalt"-kodebit (enten gjennom en logisk-ELLER uttrykk eller en JS-kommaoperator ). Den opprettholder en telling av hvor ofte Svelte kjører denne kodebiten.

I sin tur kan disse tellingene oppsummeres direkte på siden din!

monitor() metoden kan også leveres med et sett med monitorDependent parametere. Dette brukes når de pårørende du ønsker å overvåke IKKE allerede er en del av produksjonskodebiten. Teknisk sett gjør verktøyet det ikke bruke disse parameterne, snarere informerer den bare Svelte om å overvåke disse avhengige som kriterier for å gjenoppkalle kodebiten. Følgende eksempel overvåker hvor mange ganger en Svelte-butikk har endret seg:

Du kan også konsolllogge en melding, når monitor() kjøres , ved å oppgi en logMsg til skaperen:

reset() metoden kan brukes til å tilbakestille det gitte antallet.

BRUK:

Det er to forskjellige måter som ReflectiveCounter kan brukes:

  1. Overvåk html refleksive tellinger (i html-snippets):

    <script>
      const fooReflexiveCount = createReflectiveCounter('foo section fired');
    </script>
    
    <!-- diagnostic reporter -->
    <mark>{$fooReflexiveCount}:</mark>
    
    <!-- monitor this section -->
    <i>{fooReflexiveCount.monitor() || $foo}</i>
    
    <!-- reset counts -->
    <button on:click={fooReflexiveCount.reset}>Reset</button>
    
  2. Overvåk antall tilstandsendringer (i kodebiter):

    <script>
      const fooChangeCount = createReflectiveCounter();
      $: fooChangeCount.monitor($foo);
    </script>
    
    <!-- reporter/resetter -->
    <i>$foo state change counts: {$fooChangeCount}</i>
    <button on:click={fooChangeCount.reset}>Reset</button>
    

Du kan se en live-demo av ReflectiveCounters i Advanced Probes-diskusjonen.

Innsikt :Diagnostiske prober er midlertidige

Demo-app

Før vi kan begynne noen analyse, trenger vi litt kode å leke med. Det skal være enkelt og fokusert, slik at vi kan konsentrere oss om dets reaktivitet.

Jeg har laget en interaktiv demo (en Svelte REPL) som vi kan bruke.

Demo REPL :

Den grunnleggende ideen bak demoen er at du kan opprettholde egenskapene til en pålogget bruker (øverste halvdel:EditUser.svelte ) , og vis dem (nedre halvdel:GreetUser.svelte ) ... ganske enkelt :-) Du kan oppdatere en eller flere egenskaper for brukeren ved å endre teksten og klikke Apply Change knapp. Fortsett og spill med den interaktive demoen nå !

Demoen er delt opp i en rekke moduler. Jeg vil ikke detaljere dem her ... de er oppsummert i App.svelte (av demoen REPL).

Sidefelt :Normalt EditUser /GreetUser komponenter vil være gjensidig utelukkende (dvs. vist på forskjellige tidspunkter) ... jeg bare kombinert dem slik at vi bedre kan se den "refleksive korrelasjonen" mellom de to .

For diskusjonene våre vil vi fokusere på én enkelt modul:GreetUser komponent.

GreetUser.svelte (se GU1_original.svelte i Demo REPL)

<script>
 import user from './user.js';
</script>

<hr/>
<p><b>Greet User <mark><i>(original)</i></mark></b></p>

<p>Hello {$user.name}!</p>
<p>
  May we call you at:
  <i class:long-distance={$user.phone.startsWith('1-')}>
    {$user.phone}
  </i>?
</p>

<style>
 .long-distance {
   background-color: pink;
 }
</style>

Denne komponenten hilser bare den påloggede brukeren (en objektbasert Svelte-butikk) , visualiserer individuelle egenskaper til brukeren. Langdistansetelefonnumre vil bli uthevet (når de begynner med "1-") .

Hva kan være enklere enn dette? Dette bør gi et godt grunnlag for våre diskusjoner :-)

Inspiserer appreaktivitet

La oss forbedre GreetUser komponent med våre diagnostiske prober (diskutert i Exploring App Reactivity) for å se hvor godt den oppfører seg.

Loggingsonder

Her er vår GreetUser komponent med diagnostiske loggingprober brukt:

GreetUser.svelte (se GU2_logDiag.svelte i Demo REPL)

<script>
 import user from './user.js';

 // diagnostic probes monitoring reflection
 const probe1 = () => console.log('Name  section fired');
 const probe2 = () => console.log('Phone class   fired');
 const probe3 = () => console.log('Phone section fired');
</script>

<hr/>
<p><b>Greet User <mark><i>(with reflexive diagnostic logs)</i></mark></b></p>

<p>Hello {probe1() || $user.name}!</p>
<p>
  May we call you at:
  <i class:long-distance={probe2() || $user.phone.startsWith('1-')}>
    {probe3() || $user.phone}
  </i>?
</p>

<style>
 .long-distance {
   background-color: pink;
 }
</style>

Du kan kjøre denne versjonen av Demo REPL ved å velge:med refleksive diagnoselogger .

Veldig hyggelig ... ved å analysere loggene, kan vi fastslå nøyaktig når individuelle html-snippets blir utført på nytt!

Avanserte sonder

La oss også bruke avansert diagnostikk (bare for moro skyld) , for å se hvordan de ser ut:

GreetUser.svelte (se GU3_advancedDiag.svelte i Demo REPL)

<script>
 import user from './user.js';
 import createReflectiveCounter from './createReflectiveCounter.js';

 // diagnostic probes monitoring reflection
 const probe1 = createReflectiveCounter('Name  section fired');
 const probe2 = createReflectiveCounter('Phone class   fired');
 const probe3 = createReflectiveCounter('Phone section fired');
</script>

<hr/>
<p><b>Greet User <mark><i>(with advanced on-screen diagnostics)</i></mark></b></p>

<p>
  <mark>{$probe1}:</mark>
  Hello {probe1.monitor() || $user.name}!</p>
<p>
  <mark>{$probe2}/{$probe3}:</mark>
  May we call you at:
  <i class:long-distance={probe2.monitor() || $user.phone.startsWith('1-')}>
    {probe3.monitor() || $user.phone}
  </i>?
</p>

<style>
 .long-distance {
   background-color: pink;
 }
</style>

Du kan kjøre denne versjonen av Demo REPL ved å velge:med avansert skjermdiagnostikk .

Flott ... komponentens reaktivitet er nå synlig, direkte på siden vår!

Gjengivelsesanalyse

Så det ser ut til å være noen uventede resultater , avslørt gjennom introduksjonen av våre diagnostiske prober. Vi ser html-snippets kjøres på nytt når tilstanden deres IKKE endret seg (au) !

Du kan se dette ved å endre en enkelt egenskap (si navn) , og legg merke til at alle tre av html-snuttene våre kjøres på nytt! Du kan til og med klikke Apply Change knappen uten egenskapsendringer, og fortsatt ... alle tre av våre html-snippets kjøres på nytt! Sidefelt :Jeg skjønner at jeg kan optimalisere user lagre for å forhindre dette siste scenariet, men for denne diskusjonens formål fremhever det bedre punktet vi kjører på.

Så hva skjer?

Avreferanseobjekter

Hvis du husker diskusjonen vår om reaktive utløsere, er dette faktisk et eksempel på at en objektreferanse er for bred i sin avhengighetsgranularitet.

<p>Hello {$user.name}!</p>

Fordi Svelte har merket $user objekt som foreldet, vil enhver html-snipp som refererer til det objektet kjøres på nytt, uavhengig av om den derefererte .name har endret seg eller ikke!

Ved første øyekast virker dette mot intuitivt . Hvorfor ville Svelte gjøre dette? Forårsaker dette faktisk overflødige og unødvendige gjengivelser i DOM-en vår? ? ... Spoilervarsel :Ingen redundante gjengivelser forekommer, men vi vil diskutere dette i neste avsnitt!

Vel, hvis du stopper opp og tenker på dette, for at Svelte skal overvåke et objekts derefererte innhold, må det forhåndsutføre underuttrykk som finnes i kodebiten, og overvåke den resulterende verdien.

I vårt enkle eksempel kan det være teknisk mulig, men som en generell regel er dette en dårlig idé, av en rekke årsaker.

Den primære grunnen er at disse underuttrykkene alltid må utføres for å oppnå dette. , og det strider mot grunnprinsippet i det Svelte prøver å gjøre (dvs. det er den reaktive utløsningsmekanismen) ... det vil si:Skal denne kodebiten kjøres på nytt eller ikke? Hvis Svelte måtte forhåndsutføre deler av kodebiten for å gjøre denne avgjørelsen, kan det være negative bivirkninger! For eksempel kan underuttrykket være å påkalle en metode som bruker uønskede mutasjoner osv.

Sidefelt :Min forklaring her er min "beste gjetning", basert på intuisjon. Hvis jeg mottar kommentarer fra "in the know" Svelte-vedlikeholdere, vil jeg gjøre nødvendige rettelser (til forklaringen) og fjerne denne SideBar :-) Uavhengig av forklaringen, er det faktisk slik Svelte fungerer!

Sveltes re-render-optimalisering

Så hva betyr dette?

"Elefanten i rommet" er:Produserer dette faktisk overflødige og unødvendige gjengivelser i vårt DOM? Husk:DOM-oppdateringer er dyre ! Er dette virkelig sant, eller skjer det mer "under dynen" ?

Det gikk opp for meg at bare fordi Svelte bestemte seg for å kjøre html-snippet mitt på nytt, betyr det ikke nødvendigvis at det resulterte i en DOM-oppdatering.

Kan det være at Svelte optimaliserer denne prosessen ytterligere ved å forsikre at resultatet av en html-snipp faktisk er endret? Hvis du tenker deg om, er dette veldig fornuftig for Svelte å gjøre.

  • I dette spesielle tilfellet blir en unødvendig html-snipp utført på nytt på grunn av en altfor bred avhengighetsgranularitet ... dvs. et objekt viser det individuelle innholdet (vi diskuterte dette i delen Reaktive utløsere) .

  • Det er imidlertid andre tilfeller hvor html-snippet vår kan returnere det samme resultatet, selv når dets avhengigheter lovlig endres. Tenk på det:dette er applikasjonskoden (utenfor Sveltes kontroll) . Tenk på et tilfelle der appkravene våre vil gruppere et sett med oppregnede verdier i én klassifisering, og generere det samme resultatet fra flere verdier.

Som det viser seg, optimaliserer Svelte faktisk sine DOM-oppdateringer ved å sikre at innholdet faktisk har endret seg ... så det er ingen redundante gjengivelser !

Svelte kommer oss til unnsetning igjen !

Jeg bestemte dette først ved å gå inn i en feilsøkingsøkt for en av mine diagnostiske prober .

  • Ved å gå ut ett nivå (inn i Svelte-verdenen) , fant jeg meg selv i en ganske kryptisk kode, der en ganske kompleks betinget var å utføre en hjelpefunksjon som faktisk utførte lavnivå-DOM-oppdateringen.

  • Siden jeg var litt usikker på denne komplekse betingelsen, bestemte jeg meg for å bare sette et bruddpunkt på den hjelpefunksjonen.

  • Dette tillot meg å samhandle med appen min og fastslå at:sikkert nok ... DOM-fragmentene oppdateres bare (dvs. gjengi) når resultatet av html-snippet faktisk ble endret !

DETTE ER SÅÅÅ KOOOL !

Svelte Compiler Output

OK, nå begynte jeg å bli frekk . Jeg begynte å lure på:hvor effektiv er Svelte når det gjelder å bestemme denne "innholdsendring" ? Jeg tenkte stadig mer på denne kryptiske koden der jeg fant meg selv (i feilsøkingsøkten) :

Forutsetningen min var riktig !

Med denne nyvunne selvtilliten tør jeg prøve å forstå denne kryptiske koden ? ... vel, det er verdt et forsøk !

Advarsel :Denne delen er helt valgfri . Vi har allerede diskutert nøkkelen du trenger å vite om dette emnet. Derfor er denne delen kun for ekstra kreditt (om enn veldig interessant for ekte nerder) ! Gå gjerne videre til neste seksjon.

FYI :Jeg vil ikke rote til artikkelen med mye av denne kryptiske koden ... du kan følge med ved å se "JS-utdata" kategorien fra Demo REPL.

Så her kommer ...

Kryptiske navn:

Det første du vil legge merke til er at variabelnavnene i denne koden ikke er utrolig intuitive ... for det meste nummererte variabler med enkeltbokstavsprefikser. Men hei:dette er maskingenerert kode! Vi vil ikke ha lange, intuitive navn som er oppblåst på størrelse med pakken vår! Faktisk, når du først har fått taket på det, er det noen nyttige mønstre i navnene ... fortsett å lese .

DOM-fragmenter:

Det viktigste med denne koden er at Svelte har klart å bryte ned html-en vår til fragmenter som kan gjenoppbygges på det laveste nivået av DOM-treet vårt.

Dette er et avgjørende poeng ! Når dette først er oppnådd, blir det ganske trivielt å gradvis bearbeide endring!

Min intuisjon forteller meg at dette sannsynligvis er det mest komplekse aspektet ved kompilatoren.

  • For statisk html (det varierer ikke) , den bruker til og med en enkel tilnærming til innerHTML .

    For eksempel dette:

    <p><b>Greet User <mark><i>(original)</i></mark></b></p>
    

    Genererte dette:

    p0 = element("p");
    p0.innerHTML = `<b>Greet User <mark><i>(original)</i></mark></b>`;
    

    Nå dette kan jeg håndtere :-)

  • For dynamisk html-innhold (drevet av en html-snippet/interpolering) , bryter den ned html-en i de nødvendige individuelle DOM-elementene (som kan oppdateres trinnvis) .

    For eksempel dette:

    <p>Hello {$user.name}!</p>
    

    Genererte dette:

    // from the c() method ...
    p1 = element("p");
    t4 = text("Hello ");
    t5 = text(t5_value);
    t6 = text("!");
    
    // from the m() method ...
    insert(target, p1, anchor);
    append(p1, t4);
    append(p1, t5);
    append(p1, t6);
    

    Legg merke til at for dynamisk innhold holder Svelte styr på to ting:

    • t5 tekst dom-element
    • og t5_value tekstinnhold ... dette må være resultatet av html-snippet !

Navnekonvensjoner:

Begynner du å få en følelse av noen av navnekonvensjonene?

  • p er for avsnitt
  • t er for tekstnoder
  • osv.

Komponentmetoder:

Komponenten inneholder flere metoder. Når jeg gjennomgår implementeringen deres, tror jeg at jeg kan utlede følgende egenskaper:

// appears to be initializing our internal state
c() {
  ... snip snip
}

// appears to be the initial build-up of our DOM
m(target, anchor) {
  ... snip snip
}

// appears to be the incremental update of our DOM fragments
// ... THIS IS THE KEY FOCUS OF OUR REACTIVITY (analyzed below)
p(ctx, [dirty]) {
  ... snip snip
}

// appears to be removing our DOM
d(detaching) {
  ... snip snip
}

Mer om navnekonvensjoner:

Hei ... disse navnene begynner å gi mening når du først skjønner at vi spiller Sesame Street Alphabet Game!

  • c() er for constructor()
  • m() er for mount()
  • p() er for partiallyPutinProgressivePermutations() ... Jeg har tydeligvis INGEN IDE på denne :-( Mark fortalte meg senere at det står for update() (med den andre bokstaven) , og ga en referanse til en Tan Li Hau-ressurs ... hvor var dette da jeg trengte det ? :-)
  • d() er for destroy()
  • Det finnes en rekke metoder som ikke er operative (f.eks. i: noop , etc.), så vi har tydeligvis IKKE truffet de mer avanserte sakene med vår veldig enkle komponent :-)

Inkrementelle oppdateringer:

Den primære metoden vi er interessert i er p() metode. Det er her de inkrementelle DOM-oppdateringene skjer. Det var der jeg fant meg selv i feilsøkingsøkten, da jeg fant ut at DOM-oppdateringene var optimalisert.

  • Legg merke til at den har tre seksjoner med kode (hver prefiks med en betinget - if )

  • WowZee ... komponentdefinisjonen vår har også 3 html-snippets (for en tilfeldighet) !

  • La oss se på en av dem (jeg har formatert JS-en litt, og lagt til // kommentarer) :

    html-kodefragment

    <p>Hello {$user.name}!</p>
    

    kompilert utdata

    p(ctx, [dirty]) {
      // one of 3 sections ...
      if (dirty & /*$user*/ 1 &&                                  // conditional Part I
          t5_value !== (t5_value = /*$user*/ ctx[0].name + "")) { // conditional Part II
        set_data(t5, t5_value);                                   // the payload - update the DOM!
      }
      ... snip snip
    },
    

Her er min analyse:

  • ctx[] array inneholder alle våre avhengigheter. ctx[0] tilfeldigvis er $user vår objekt (takket være kompilatorens beholdte kommentarhint) .

  • dirty inneholder en bitvis akkumulering av "staleness" av ALLE våre avhengige variabler (en bit for hver avhengig) .

  • Del I av betinget trekker ut det skitne flagget til $user avhengig variabel (ved hjelp av den bitvise AND-operatoren - & ) . Dette avgjør om vår $user variabelen er foreldet. Hvis det er det, fortsetter vi til del II (via logical-AND). operatør - && ).

  • Del II av betingelsen gjør faktisk to ting:Den tildeler den siste t5_value fra html-snippet vår (etter å ha konvertert den til en streng:+ "" ) , OG den sammenligner forrige/neste kodeutdata (ved hjelp av identitetssemantikk:!== ). Bare når forrige/neste er endret, vil den utføre den betingede nyttelasten (dvs. oppdatere DOM ) . Til syvende og sist er dette betingede en veldig enkel primitiv strengsammenligning!

  • set_data() function er et Svelte-hjelpeverktøy som faktisk oppdaterer DOM! Du kan finne disse verktøyene på GitHub her, eller ganske enkelt åpne dem fra din installerte node_modules/svelte/internal/index.js . Dette spesielle verktøyet setter bare de leverte dataene i et DOM-tekstelement:

  function set_data(text, data) {
    data = '' + data;
    if (text.data !== data)
      text.data = data;
  }

Sveltes refleksivitet er veldig effektivt

Vel, det var gøy! En veldig interessant øvelse ! Hva har vi lært ?

  • Ikke vær redd for å åpne "JS-utgangen" kategorien i REPL!

  • Big Bird ville gjort det bra i en Svelte-kodeanmeldelse!

  • Viktigst , følgende innsikt :

Innsikt :Sveltes refleksivitet er veldig effektiv!

Hvem kan be om noe mer ?

Kudos gå ut til Rich Harris og kjernebidragsyterne for å være så smarte og grundige!

Appreaktivitetsjusteringer

Vi har lært at det er et subtilt skille mellom refleksjon (Sveltes utførelse av html-snippets) og gjengjengivelse (bruker DOM-oppdateringer) .

Bare fordi Svelte har bestemt seg for å kjøre en html-snippet (gjennom dets avhengighetsovervåking) , betyr ikke at en DOM-oppdatering brukes (selv om det vanligvis er det) ... fordi kodebiten kan returnere det samme resultatet . Svelte optimaliserer denne prosessen for å sikre at DOM-oppdateringer bare skjer når de faktisk endres.

Som et resultat kan refleksjonstallene våre være litt større enn gjengivelsestallet. Det er to grunner til dette:

  1. En altfor bred avhengighetsgranularitet (f.eks. forskjellen mellom objekter og primitiver) . Denne er på skuldrene til Svelte. Som et eksempel har Svelte påkalt kodebiten vår på grunn av en objektendring, men objektets underinnhold (brukt av kodebiten vår) har egentlig ikke endret seg. Vi vil diskutere dette videre i:Finer Grained Dependency Management

  2. HTML-snippet kan returnere det samme resultatet for flere avhengige verdier. Dette er på skuldrene til appen vår. Tenk på tilfellet der appkravene våre vil gruppere et sett med oppregnede verdier i én klassifisering, og generere det samme resultatet fra flere verdier. Vi vil diskutere dette videre i:Presolve Variations

Uavhengig av hvem som står på disse forholdene, finnes det appspesifikke teknikker som vi kan redusere dette gapet med (til og med null) . Så hvordan kan vi påvirke dette? Tross alt er Svelte den som har kontroll over å utføre html-snippets. Hvordan kan vi endre dette?

Den grunnleggende drivkraften i det vi skal gjøre er å flytte en del av refleksivitet FRA html-snippets TIL kode-snippets . Husk at vi nevnte at kodebiter vanligvis har mindre overhead (fordi de bare resulterer i endringer i JavaScript-tilstandsvariabler) .

Hvorfor vil du gjøre dette? Representerer det virkelig en betydelig optimalisering? Vel, tenk på dette:

  1. Hva om dette avviksantallet var stort (der vi unødvendig utførte en html-snippet på nytt mange ganger med samme utdata) ?
  2. Og hva om kostnadene ved å kjøre denne html-snippet var ekstremt høye?
  3. Hva om den samme html-snippet var nødvendig på flere steder i vår html?

Husk at vi ikke har disse betingelsene i vår enkle leketøysapp ... men for eksempel, la oss late som om vi gjør det!

Når det gjelder optimaliseringsspørsmålet ... for å være ærlig, mest sannsynlig vil ikke teknikkene vi skal diskutere nevneverdig påvirke appens ytelse. I noen tilfeller vil vi bare flytte en optimalisering som Svelte allerede gjorde, til applikasjonsområdet. Når det er sagt, er den beste muligheten for en optimalisering punkt 3 (over).

Så hvorfor gå gjennom denne øvelsen? Veldig enkelt: for bedre å forstå de finere egenskapene til Sveltes reaktivitet ! Denne kunnskapen kan gi deg fordelen som skiller seniorutviklerne ... vite virkningen av finkornede justeringer ... trekke inn de store pengene ... vi kan bare håpe !

Ekstrem optimalisering :Foretrekker reaktivitet i kodebiter vers html-snippets

Finer Grained Dependency Management

This section addresses Svelte's overly broad dependency granularity, as it relates to Primitive Types verses Object Types.

Our GreetUser component is currently dereferencing the $user object within it's html. This is causing Svelte to execute our html-snippets in cases where the dereferenced property has not changed.

We can change this by simply normalizing our referenced state into primitive types.

Takeaway :Fine Tune Svelte's Dependency Management by using primitive types

Here is our GreetUser component with the applied change:

GreetUser.svelte (see GU4_primNorm.svelte in Demo REPL)

<script>
 import user from './user.js';
 import createReflectiveCounter from './createReflectiveCounter.js';

 // FOCUS: with primitive normalization
 // normalize our referenced state with primitive types
 // ... html-snippets will only fire when values actually change
 // ... using JS identity semantics
 $: ({name, phone} = $user);

 // diagnostic probes monitoring reflection
 const probe1 = createReflectiveCounter('Name  section fired');
 const probe2 = createReflectiveCounter('Phone class   fired');
 const probe3 = createReflectiveCounter('Phone section fired');
</script>

<hr/>
<p><b>Greet User <mark><i>(with primitive normalization)</i></mark></b></p>

<p>
  <mark>{$probe1}:</mark>
  Hello {probe1.monitor() || name}!</p>
<p>
  <mark>{$probe2}/{$probe3}:</mark>
  May we call you at:
  <i class:long-distance={probe2.monitor() || phone.startsWith('1-')}>
    {probe3.monitor() || phone}
  </i>?
</p>

<style>
 .long-distance {
   background-color: pink;
 }
</style>

You can run this version of the Demo REPL by selecting:with primitive normalization .

Great :Notice that the reflection counts (Svelte's execution of html-snippets) now correctly reflect actual changes to the corresponding state!

In this example, the "primitive normalization" was accomplished in the component code-snippet:

$: ({name, phone} = $user);

When the $user object changes, this normalization code will be re-executed. However, because our html-snippets utilize the name /phone primitives, only the snippets that depend on the properties that truly changed will re-execute! ... very kool!

This "primitive normalization" can be accomplished in a variety of ways. In our example, it was carried out in the component code. Another way you could accomplish this is to promote derived stores , that pull a single value out. For eksempel:

user.js (modified)

import {writable, derived} from 'svelte/store';

export const user = writable({
  name:  '',
  phone: '',
});

export const name  = derived(user, (u) => u.name);
export const phone = derived(user, (u) => u.phone);

Preresolve Variations

This section addresses the case where an html-snippet generates the same result for multiple dependent values. This typically occurs when the snippet contains conditional logic.

In our example, long distance phone numbers will be highlighted (when they begin with "1-") . This is accomplished by conditional logic in the html-snippet:

<i class:long-distance={phone.startsWith('1-')}>
  ... snip snip
</i>

The issue here is that Svelte will re-execute the html-snippet based on whether the dependent phone changes, irrespective of whether the CSS class will change.

You can see this in the demo by changing the latter part of the phone number (keeping the prefix intact):

As you can see, this resulted in a higher number of reflection counts (Svelte's execution of html-snippets) .

Solution:

If we were to move this logical condition into a code-snippet, the resulting html-snippet would result in fewer executions!

Takeaway :Fine Tune conditional logic by moving html-snippet variations into code-snippets

Here is our GreetUser component with the applied change:

GreetUser.svelte (see GU5_variations.svelte in Demo REPL)

<script>
 import user from './user.js';
 import createReflectiveCounter from './createReflectiveCounter.js';

 // normalize our referenced state with primitive types
 // ... html-snippets will only fire when values actually change
 // ... using JS identity semantics
 $: ({name, phone} = $user);

 // FOCUS: with variations in code
 // by allowing conditional expressions to be resolved in a code-snippet,
 // the resulting html-snippet will fire less often.
 $: classes = phone.startsWith('1-') ? 'long-distance' : '';

 // diagnostic probes monitoring reflection
 const probe1 = createReflectiveCounter('Name  section fired');
 const probe2 = createReflectiveCounter('Phone class   fired');
 const probe3 = createReflectiveCounter('Phone section fired');
</script>

<hr/>
<p><b>Greet User <mark><i>(with variations in code)</i></mark></b></p>

<p>
  <mark>{$probe1}:</mark>
  Hello {probe1.monitor() || name}!</p>
<p>
  <mark>{$probe2}/{$probe3}:</mark>
  May we call you at:
  <i class="{probe2.monitor() || classes}">
    {probe3.monitor() || phone}
  </i>?
</p>

<style>
 .long-distance {
   background-color: pink;
 }
</style>

You can run this version of the Demo REPL by selecting:with variations in code .

Great :Notice that the reflection counts (Svelte's execution of html-snippets) now correctly reflects whether the CSS class actually changed!

In this rendition, the variability is now accomplished in the component code-snippet:

$: classes = phone.startsWith('1-') ? 'long-distance' : '';

As a result, the html-snippet will only execute when the classes variable actually changes.

Optimization Caveats

Here are a couple of "extras" to consider regarding optimization:

Insight :Optimization is only relevant when reactivity occurs for active components

Insight :Optimization is preferred but optional

Extra Credit Exercise

For those who would like some extra credit , let me propose an enhancement to our ReflectiveCounter (discussed in Advanced Diagnostics) .

In it's current form, the ReflectiveCounter is providing us a reflexive count (the html-snippet execution count) .

Can you think of a way that it could provide both reflexive counts -and- re-render counts (that is ... of the DOM updates) ?

This little exercise should separate the Geeks from the wannabes !

I won't give you the solution directly, but here is a very big hint ... The invocation will change:

FROM:

<i>{fooProbe.monitor() || $foo}</i>

TO:

<i>{fooProbe.monitor( () => $foo )}</i>

Are you up for the challenge? FYI:There is a hidden easter egg (tucked away somewhere) that reveals the solution! If you can't find it, just ping me in the comments below .

Who is this guy?

Just to give you a little of my background (as it relates to software engineering) ...

I have been in the software industry for over 40 years. I'm probably the old guy in the room (retired since 2015) . I like to say that I am a "current" developer from a different era , but gee whiz, it is getting harder and harder to stay current! Case in point:I'm just now learning Svelte, which has been out how long ?

Needless to say, I cut my "programming teeth" 25 years before there was a usable internet (in the mid 70's) .

I remember the great computing pioneer, Grace Hopper as a visiting lecturer, who at the age 73 imparted the computing insights of the day (which at it's core, wasn't all that different from today) . She used great visual aids ... passing out nanoseconds , etc. Admiral Hopper was a senior way back then (in the mid 70's) , so I suppose I shouldn't be too self conscious :-) Trivia point :she also coined the term:bug !

When I eventually started web-development (in the mid 90's) , I was "all in" for this new Netscape technology called JavaScript! Even back then, we were providing reactivity at a page level, using this new innovation.

Over the years I have written a number of large-scaled SPAs (predating the SPA term) , using pure JavaScript (i.e. there were no frameworks) ! Believe me, providing large-scaled app-based reactivity is a daunting task, requiring some good underlying architecture, and ultimately a lot of code!

I actually skipped right over the jQuery phenomenon, and went straight into the new declarative frameworks ... first Angular, then React. This declarative approach never ceases to amaze me ... in realizing how much can be accomplished with so little code :-)

Svelte merely takes this progression to the next level ! It provides all the benefits of a declarative approach, without the bloated in-memory run-time framework!

I have been contributing to open source since my retirement (in 2015) . My most recent offering is a product called feature-u:a React utility that facilitates Feature-Driven Development .

I am a brand spanking new Svelter !

My first Svelte project (too early to publish) is a re-creation of my most prized project (in the early 90's) . It was an "Engineering Analysis" tool, written in C++ under Unix/X-Windows. It had:

  • schematic capture:with multiple functional decompositions of the master schematic
  • executable control laws:through graphical flow diagrams that were executable
  • simulation:driven by the control laws (animating one or more of the schematics and control laws)
  • a symbolic debugger:also driven by the control laws
  • auto generation of the embedded system code (derived from the executable control laws)
  • Needless to say, this system has reactivity on steroids !

You can find me On The Web, LinkedIn, Twitter, and GitHub.

Summary

Well, this turned out to be a much "deeper dive" than what I had initially envisioned :-) We have covered a lot! I hope you enjoyed this little journey, and learned something as well!

A big thanks goes out to Rich Harris and the Core Contributors for making Svelte such an awesome product! I can't wait to see what the future holds in the next release!

Happy Computing,

</Kevin>

P.S. For your convenience, I have summarized my findings here. Each point contains a short synopsis, and is linked to the more comprehensive discussion.

  1. Terminology :snippet, code-snippet, and html-snippet

  2. Insight :Reactivity is based on change in dependent state

  3. Takeaway :Monitor Svelte snippet invocations through logically-ORed prefixed expressions

  4. Insight :Diagnostic probes are temporary

  5. Insight :Svelte's reflexivity is very efficient!

  6. Extreme Optimization :Prefer reactivity in code-snippets verses html-snippets

  7. Takeaway :Fine Tune Svelte's Dependency Management by using primitive types

  8. Takeaway :Fine Tune conditional logic by moving html-snippet variations into code-snippets

  9. Insight :Optimization is only relevant when reactivity occurs for active components

  10. Insight :Optimization is preferred but optional