Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Zev Averbach
Zev Averbach

Posted on • Edited on

     

How Does Svelte Actually Work? part 1

Here's part 2:

A friend put Svelte on the map for me this summer. Rather than tout its performance relative to the frameworks of the day, he touted the bite-sizedness and readability of the JavaScript it generates when compiled.

I'm writing a course that uses Svelte (and FastAPI and some other snazzy things) and am realizing that I could use some deeper knowledge of how Svelte operates: Specifically, how the code works that Svelte compiles to.

I'll post my insights as they come about, so this is part 1 ofx.

First Steps

I used the template provided by the Svelte project by doing
npx degit sveltejs/template my-svelte-project; cd $_; npm install.

Then I rannpm run dev to compile the included component and start the development server.

This producedbuild/bundle.js, the beast we'll be dissecting.

Start at the Bottom

// build/bundle.js (all code blocks are from this file unless otherwise specified)...constapp=newApp({target:document.body,props:{name:'world'}});returnapp;}());//# sourceMappingURL=bundle.js.map
Enter fullscreen modeExit fullscreen mode

I didn't know what a source map is, but having Googled it and inspectedbundle.js.map a little, I've decided not to try to decipher it just yet!

Those parens at the end tell me that theapp var on line 3 ofbundle.js

...varapp=(function(){...
Enter fullscreen modeExit fullscreen mode

stores the result ofreturn app, as everything on the right-hand side of that 👆👆= is an anonymous function which immediately calls itself.

Then, the above block, starting withconst app, is identical to the logic inmain.js.

// src/main.jsimportAppfrom'./App.svelte';constapp=newApp({target:document.body,props:{name:'world',}});exportdefaultapp;
Enter fullscreen modeExit fullscreen mode

Searching formain.js in the Rollup config file that came with this sample app, I see

// rollup.config.js...input:'src/main.js',...
Enter fullscreen modeExit fullscreen mode

Okay, I'm reminded that this is where the Svelte app is defined, as configured inrollup.config.js.

The App: First Hypothesis

It looks like theApp class hasget andset methods on it, each calledname.

...classAppextendsSvelteComponentDev{constructor(options){super(options);init(this,options,instance,create_fragment,safe_not_equal,{name:0});dispatch_dev("SvelteRegisterComponent",{component:this,tagName:"App",options,id:create_fragment.name});const{ctx}=this.$$;constprops=options.props||({});if(/*name*/ctx[0]===undefined&&!("name"inprops)){console.warn("<App> was created without expected prop 'name'");}}getname(){thrownewError("<App>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");}setname(value){thrownewError("<App>: Props cannot be set directly on the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");}}...
Enter fullscreen modeExit fullscreen mode

I hypothesize that if I giveApp another prop, there will be a pair ofget andset for that as well.

Testing Hypothesis #1

<!-- src/App.svelte --><script>exportletname;exportletnumber;// new</script>
Enter fullscreen modeExit fullscreen mode

Sure enough, these methods have appeared:

...getname(){thrownewError("<App>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");}setname(value){thrownewError("<App>: Props cannot be set directly on the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");}getnumber(){thrownewError("<App>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");}setnumber(value){thrownewError("<App>: Props cannot be set directly on the component instance unless compiling with 'accessors: true' or '<svelte:options accessors/>'");}...
Enter fullscreen modeExit fullscreen mode

So that's how that works. I don't know much about how getters/setters work in JS classes, but I'm guessing it's like in Python: They trigger when you try to get or set an instance attribute.

Then there's this in the constructor ofApp:

if(/*name*/ctx[0]===undefined&&!("name"inprops)){console.warn("<App> was created without expected prop 'name'");}if(/*number*/ctx[1]===undefined&&!("number"inprops)){console.warn("<App> was created without expected prop 'number'");}
Enter fullscreen modeExit fullscreen mode

Thisctx thing is mysterious, and it's popped off of the even more mysteriousthis.$$.

classAppextendsSvelteComponentDev{constructor(options){...const{ctx}=this.$$;...
Enter fullscreen modeExit fullscreen mode

We'll come back to these.

Before continuing, let's updatemain.js to provide a value for thenumber prop.

// src/main.js...constapp=newApp({target:document.body,props:{name:'world',number:42}});
Enter fullscreen modeExit fullscreen mode

Everything Starts increate_fragment

functioncreate_fragment(ctx){letmain;leth1;lett0;lett1;lett2;lett3;letp;lett4;leta;lett6;constblock={c:functioncreate(){main=element("main");h1=element("h1");t0=text("Hello");t1=text(/*name*/ctx[0]);t2=text("!");t3=space();p=element("p");t4=text("Visit the");a=element("a");a.textContent="Svelte tutorial";t6=text(" to learn how to build Svelte apps.");attr_dev(h1,"class","svelte-1tky8bj");add_location(h1,file,5,1,46);attr_dev(a,"href","https://svelte.dev/tutorial");add_location(a,file,6,14,83);add_location(p,file,6,1,70);attr_dev(main,"class","svelte-1tky8bj");add_location(main,file,4,0,38);},l:functionclaim(nodes){thrownewError("options.hydrate only works if the component was compiled with the `hydratable: true` option");},m:functionmount(target,anchor){insert_dev(target,main,anchor);append_dev(main,h1);append_dev(h1,t0);append_dev(h1,t1);append_dev(h1,t2);append_dev(main,t3);append_dev(main,p);append_dev(p,t4);append_dev(p,a);append_dev(p,t6);},p:functionupdate(ctx,[dirty]){if(dirty&/*name*/1)set_data_dev(t1,/*name*/ctx[0]);},i:noop,o:noop,d:functiondestroy(detaching){if(detaching)detach_dev(main);}};dispatch_dev("SvelteRegisterBlock",{block,id:create_fragment.name,type:"component",source:"",ctx});returnblock;}
Enter fullscreen modeExit fullscreen mode

create_fragment is a function that takes a single argumentctx, and its job is primarily to create and render DOM elements; it returnsblock.

block

block is an object whose most important attributes arec (create),m (mount),p (update),d (destroy).

c (create)

block.c's value is a factory function calledcreate, which

c:functioncreate(){main=element("main");h1=element("h1");t0=text("Hello");t1=text(/*name*/ctx[0]);t2=text("!");t3=space();p=element("p");t4=text("Visit the");a=element("a");a.textContent="Svelte tutorial";t6=text(" to learn how to build Svelte apps.")...
Enter fullscreen modeExit fullscreen mode

1) creates a bunch of DOM elements and text nodes
2) assigns them each to a variable declared at the start ofcreate_fragment

Then it

...attr_dev(h1,"class","svelte-1tky8bj");add_location(h1,file,5,1,46);attr_dev(a,"href","https://svelte.dev/tutorial");add_location(a,file,6,14,83);add_location(p,file,6,1,70);attr_dev(main,"class","svelte-1tky8bj");add_location(main,file,4,0,38);}
Enter fullscreen modeExit fullscreen mode

3) sets attributes (like 'class' and 'href') on the elements
4) dispatches an event for each attribute-setting (more on that later: we can safely ignore these events forever).
5) adds metadata to each element (__svelte_meta) detailing exactly where it's defined in thesrc modules.

m (mount)

block.m's value is a factory function calledmount, which, y'know, adds each element and text node to the DOM in the appropriate place.

m:functionmount(target,anchor){insert_dev(target,main,anchor);append_dev(main,h1);append_dev(h1,t0);append_dev(h1,t1);append_dev(h1,t2);append_dev(main,t3);append_dev(main,p);append_dev(p,t4);append_dev(p,a);append_dev(p,t6);},
Enter fullscreen modeExit fullscreen mode

p (update)

block.p's value isnot a factory function, but a plain old function which seems to

p:functionupdate(ctx,[dirty]){if(dirty&/*name*/1)set_data_dev(t1,/*name*/ctx[0]);},
Enter fullscreen modeExit fullscreen mode

1) do something with bits that I don't understand, but probably just checks whether there's anything to update (dirty)
2) if the new value (ctx[0]) differs fromt1's value (undefined by default),
3) updatet1's value -- it's a text node, as a reminder

Hypothesis #2

I notice here that the prop we added in the first hypothesis,number, doesn't appear in theupdate function. I'm thinking this is because it's not used anywhere in the component: It's an unused prop.

Testing Hypothesis #2

<!-- src/App.svelte -->...<main><h1>Hello {name}!</h1><p>Your lucky number is {number}.</p><!-- 👈👈👈 new --><p>Visit the<ahref="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p></main>...
Enter fullscreen modeExit fullscreen mode
// build/bundle.js...p:functionupdate(ctx,[dirty]){if(dirty&/*name*/1)set_data_dev(t1,/*name*/ctx[0]);if(dirty&/*number*/2)set_data_dev(t5,/*number*/ctx[1]);},...
Enter fullscreen modeExit fullscreen mode

Ding ding ding! I'm still not sure about thisif (dirty & 2) business; we'll kick that can for now.

d (destroy)

block.d's value is a function which -- shock and awe -- removes an element from the DOM.

d:functiondestroy(detaching){if(detaching)detach_dev(main);
Enter fullscreen modeExit fullscreen mode

Where isblock consumed?

create_fragment is only called once inbundle.js, which makes sleuthing pretty easy:

...$$.fragment=create_fragment?create_fragment($$.ctx):false;...
Enter fullscreen modeExit fullscreen mode

This is inside of the monsterinit function, which is itself called only in the constructor of theclass App definition. What is thiscreate_fragment ? ... ternary about? It seems likecreate_fragment will always be truthy, given that it... exists? The more fruitful question is probably where and how is$$.fragment used? Where? In three places, it turns out. How?

init

...functioninit(component,options,instance,create_fragment,not_equal,props,dirty=[-1]){constparent_component=current_component;set_current_component(component);constprop_values=options.props||{};const$$=component.$$={fragment:null,ctx:null,// stateprops,update:noop,not_equal,bound:blank_object(),// lifecycleon_mount:[],on_destroy:[],before_update:[],after_update:[],context:newMap(parent_component?parent_component.$$.context:[]),// everything elsecallbacks:blank_object(),dirty};letready=false;$$.ctx=instance?instance(component,prop_values,(i,ret,value=ret)=>{if($$.ctx&&not_equal($$.ctx[i],$$.ctx[i]=value)){if($$.bound[i])$$.bound[i](value);if(ready)make_dirty(component,i);}returnret;}):[];$$.update();ready=true;run_all($$.before_update);// `false` as a special case of no DOM component$$.fragment=create_fragment?create_fragment($$.ctx):false;if(options.target){if(options.hydrate){// eslint-disable-next-line @typescript-eslint/no-non-null-assertion$$.fragment&&$$.fragment.l(children(options.target));}else{// eslint-disable-next-line @typescript-eslint/no-non-null-assertion$$.fragment&&$$.fragment.c();}if(options.intro)transition_in(component.$$.fragment);mount_component(component,options.target,options.anchor);flush();}set_current_component(parent_component);}...
Enter fullscreen modeExit fullscreen mode

$$.fragment is referred to three times directly after its creation ininit. Since onlytarget is in theoptions of the sample app, we'll ignore all but the second,$$.fragment && $$.fragment.c();. Similar to the previous step, I don't understand the boolean check here of$$.fragment && ..., but what's notable is thatfragment'sc method is called, which will create—but not mount—all the elements and text nodes, giving the elements metadata about their pre-compiled location inApp.svelte.

Sinceinit is called inside the constructor ofApp, we know the above will be executed at runtime.

Backtracking: What About$$?

Real quick:$$ is defined early ininit.

...const$$=component.$$={fragment:null,ctx:null,// stateprops,update:noop,not_equal,bound:blank_object(),// lifecycleon_mount:[],on_destroy:[],before_update:[],after_update:[],context:newMap(parent_component?parent_component.$$.context:[]),// everything elsecallbacks:blank_object(),dirty};...
Enter fullscreen modeExit fullscreen mode

Mystery solved!

update

functionupdate($$){if($$.fragment!==null){$$.update();run_all($$.before_update);$$.fragment&&$$.fragment.p($$.ctx,$$.dirty);$$.dirty=[-1];$$.after_update.forEach(add_render_callback);}}
Enter fullscreen modeExit fullscreen mode

We can ignore almost all of this.$$.update is assigned tonoop which does nothing at all. We'll also assume$$.fragment isn't null (how could it be??). Then,$$.before_update is currently an empty array, so we'll wait for more app complexity before studyingrun_all($$.before_update). Similarly,$$.after_update.forEach(add_render_callback) we can ignore because$$.after_update is also an empty array.

That leaves only

$$.fragment&&$$.fragment.p($$.ctx,$$.dirty);$$.dirty=[-1];
Enter fullscreen modeExit fullscreen mode

Looking aroundbundle.js I'm pretty confident that$$.dirty = [-1] means there are no pending changes to the app's state. This means that after updating the DOM in the line above it,$$.fragment.p($$.ctx, $$.dirty), we're indicating that all necessary changes have been made.

That makes the only action-packed line$$.fragment.p($$.ctx, $$.dirty), to update the DOM with any changes to
$$.ctx.

$$.ctx

$$.ctx seems to be where the app's state lives. Its calculation is a little complex:

$$.ctx=instance?instance(component,prop_values,(i,ret,value=ret)=>{if($$.ctx&&not_equal($$.ctx[i],$$.ctx[i]=value)){if($$.bound[i])$$.bound[i](value);if(ready)make_dirty(component,i);}returnret;})
Enter fullscreen modeExit fullscreen mode

Theinstance function is what generates it:

functioninstance($$self,$$props,$$invalidate){let{name}=$$props;let{number}=$$props;constwritable_props=["name","number"];Object.keys($$props).forEach(key=>{if(!~writable_props.indexOf(key)&&key.slice(0,2)!=="$$")console.warn(`<App> was created with unknown prop '${key}'`);});$$self.$set=$$props=>{if("name"in$$props)$$invalidate(0,name=$$props.name);if("number"in$$props)$$invalidate(1,number=$$props.number);};$$self.$capture_state=()=>{return{name,number};};$$self.$inject_state=$$props=>{if("name"in$$props)$$invalidate(0,name=$$props.name);if("number"in$$props)$$invalidate(1,number=$$props.number);};return[name,number];}
Enter fullscreen modeExit fullscreen mode

instance destructures our props,name andnumber, and passes them right through, unchanged, to$$.ctx.

Therefore,$$.ctx is equal to["world", 42]: Not as complex as I expected; we'll come back to all these side effects happening here between the seeming pass-through of props.

As seen earlier,$$.fragment.p($$.ctx, $$.dirty) is calling this function:

functionupdate(ctx,[dirty]){if(dirty&/*name*/1)set_data_dev(t1,/*name*/ctx[0]);if(dirty&/*number*/2)set_data_dev(t5,/*number*/ctx[1]);}
Enter fullscreen modeExit fullscreen mode

Okay, time to figure out what thisdirty & x business is about. It seems likedirty contains indices of what elements need updating, but why not find out the specifics?:

p:functionupdate(ctx,[dirty]){if(dirty&/*name*/1){console.log(`dirty 1 was dirty:${dirty}`)set_data_dev(t1,/*name*/ctx[0]);}else{console.log(`dirty 1 wasn't dirty:${dirty}`)}if(dirty&/*name*/2){console.log(`dirty 2 was dirty:${dirty}`)set_data_dev(t5,/*name*/ctx[0]);}else{console.log(`dirty 2 wasn't dirty:${dirty}`)}console.log(typeofdirty)},
Enter fullscreen modeExit fullscreen mode

In order to triggerupdate without building some UI, to trigger these informativeconsole.logs, we need to manipulate the app's state manually:

app in Action

Circling back to theinstance function, the more meaningful work it performs (the "side effects") is in binding three methods—$set,$capture_state, and$inject_state—to$$self, which isApp.

Did I mention we can inspect ourApp instance,app, in the console? It's another lovely feature of Svelte: Since it compiles down to vanilla Javascript,app is in the global scope of a browser rendering it, without any special plugins or other somersaults! Armed with that knowledge, let's play with these new methods in the Javascript console:

>>app.$capture_state()Object{name:"world",number:42}>>app.$set({name:"Whirl"})undefineddirty1wasdirty:1dirty2wasn't dirty: 1   number>> app.$capture_state()   ► Object { name: "Whirl", number: 42 }>> app.$inject_state({number: 24})   undefined   undefined   dirty 1 wasn'tdirty:2dirty2wasdirty:2number>>app.$capture_state()Object{name:"Whirl",number:24}
Enter fullscreen modeExit fullscreen mode

The page looks like this now:

A screenshot showing that the updated props have also changed in the rendered page.

Several discoveries here:

1)$capture_state gives the current state of the app as an object.
2)$set and$inject_state seem to both update the app's state via an object.
3)dirty, when it's not equal to[-1], is a positive integer seemingly referring to the props by a 1-based index.
4) These props are updated in the rendered page.

One more mystery to unravel:

>>app.nameError:<App>:Propscannotbereaddirectlyfromthecomponentinstanceunlesscompilingwith'accessors: true'or'<svelte:options accessors/>'>>app.name='hi'Error:<App>:Propscannotbesetdirectlyonthecomponentinstanceunlesscompilingwith'accessors: true'or'<  svelte:options accessors/>'
Enter fullscreen modeExit fullscreen mode

That's the purpose of theset andget methods from earlier: Enforce that the compiled code doesn't set and get props directly on theApp instance, but that it uses... the included machinery?

Next Time

Join us next time to unwrap the mysteries of

1) What is the difference betweenapp.$set andapp.$inject_state, if any?
2) How doesbundle.js change with increasing app complexity? Multiple components, for example, or dynamically re-rendering props/state.
3) What is__svelte_meta for?
4) Where and when doesmount actually get called?
5) Candirty ever contain anything besides a single integer? In other words, are elements updated one after the next, or canupdate sometimes operate on more than one element at a run?
6) When are components and elements destroyed? Are Svelte and Rollup as efficient about unnecessary re-renders as billed?
7) How does all this fit together? Asked another way, is it possible to have a basic understanding of how a web framework we use actually works?

Random Notes

According to Svelte's tweet response to me, the events emitted at various points inbundle.js are strictly for dev tooling. This is why we can ignore them.

Top comments(7)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
glebec profile image
Gabriel Lebec
  • Location
    New York City
  • Education
    B.A. Math, Studio Art (Georgetown University)
  • Work
    Software Engineer at Google
  • Joined
• Edited on• Edited

Oh man, Svelte uses bitmasks? That is the first time I have seen someone use them in prod. A bitmask is a compact way to store N boolean flags in a single N-bit integer. JS numbers are IEEE 754 64-bit floats, but when you do certain ops they are shortened to 32 bits, so you can safely store 32 bools in a single JS number. When you& x, you check if the digit in place 2x-1 is a 1 or a 0. Supposedirty = 13; then in binary,dirty is 1101. If you dodirty & 1 you get1 (the lowest bit). If you dodirty & 2 you get0 (the second lowest bit). So if you use the lowest bit to store whethername is dirty, and the second lowest bit to store whethernumber is dirty, then in the case ofdirty = 13, they are dirty and not dirty respectively!

This also means that if you need to store more than 32 prop dirty states, Svelte will need more numbers to cram the states in as bits. But if this is per-component, I doubt many people exceed 32 props in a component. Neat!

CollapseExpand
 
zev profile image
Zev Averbach
Programmer. I like building things around the spoken word.
  • Location
    Switzerland
  • Education
    UC Berkeley
  • Work
    Senior Data Engineer at Caterpillar, Inc.
  • Joined

Thanks Gabriel! Mystery solved, once I wrap my head around this.

CollapseExpand
 
glebec profile image
Gabriel Lebec
  • Location
    New York City
  • Education
    B.A. Math, Studio Art (Georgetown University)
  • Work
    Software Engineer at Google
  • Joined

Two good resources for understanding bitmasks in this context:

CollapseExpand
 
mrgnw profile image
Morgan
Everything is a system, but not everything should be a machine.
  • Location
    Barcelona
  • Pronouns
    he/him
  • Work
    Software Engineer
  • Joined

This is great! Can't wait to see how things go with FastAPI & the other snazzy things

CollapseExpand
 
zev profile image
Zev Averbach
Programmer. I like building things around the spoken word.
  • Location
    Switzerland
  • Education
    UC Berkeley
  • Work
    Senior Data Engineer at Caterpillar, Inc.
  • Joined

Aw shucks! Did you look at part 2? I need any feedback you've got. :-)

CollapseExpand
 
mrgnw profile image
Morgan
Everything is a system, but not everything should be a machine.
  • Location
    Barcelona
  • Pronouns
    he/him
  • Work
    Software Engineer
  • Joined

I’ll read it tomorrow!
If you haven’t been on the svelte discord they might be able to add feedback on the svelte side of things.

CollapseExpand
 
caroso1222 profile image
Carlos Roso
Software Engineer. Former digital nomad at Toptal. Open sorcerer. Thoughts on career growth, remote work, and web dev.
  • Joined

Pure gold here. Thanks for the breakdown! I also found it interesting how they use bitmasks to check for dirty states. Analyzing the compiler might also be a fun exercise.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Programmer. I like building things around the spoken word.
  • Location
    Switzerland
  • Education
    UC Berkeley
  • Work
    Senior Data Engineer at Caterpillar, Inc.
  • Joined

More fromZev Averbach

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp