Here's part 1:
How Does Svelte Actually Work? part 1
Zev Averbach γ» Dec 5 '19 γ» 11 min read
and here's the repository for what we've done so far:
zevaverbach / how-svelte-works
Contains the markdown and code for the blog series.
(hint: we have barely changed Svelte's sample app).
Episode Recap
On the previous episode of HDSAW we took a bewildering ride through the 400 or so lines of vanilla JavaScript the Svelte compiler produces from the sample app. Here's that vanilla JS for your perusal, and see below for an updated version.
What We've Learned So Far
When we run npm run dev
to compile the sample app, Rollup takes the contents of main.js
and runs the Svelte compiler on it. This produces a file called bundle.js
in the build
directory.
bundle.js
defines and immediately calls an anonymous function, creating an instance of App
called app
. The code which runs inside the constructor of App
first renders the Svelte app, then binds a small handful of methods to the instance. These methods can be used to get and set the state of the app, including by the user in the browser's JavaScript console!
Outstanding Questions
1) Is it a security problem that the user can directly manipulate the app's state from a browser's JavaScript console?
2) What is the difference between app.$set
and app.$inject_state
, if any?
3) How does bundle.js
change with increasing app complexity? Multiple components, for example, or dynamically re-rendering props/state.
4) What is __svelte_meta
for?
5) Where and when does mount
actually get called?
6) Can dirty
ever contain anything besides a single integer? In other words, are elements updated one after the next, or can update
sometimes operate on more than one element at a run?
7) When are components and elements destroyed? Is Svelte as efficient about unnecessary re-renders as billed?
8) What are the setters and getters on App
for and why are they implemented the way they are?
9) 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?
We'll try to get through a few of these today, and then I'm sure more questions will emerge as we go.
A Skinnier Bundle
This time around we're going to generate the build.js
using npm run build
, so as to not distract ourselves with the events that get emitted in dev mode. First, though, let's tell Rollup not to minify that file so we can actually read it:
// ./rollup.config.js
...
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
// production && terser() πππ comment this out
],
watch: {
...
npm run build
produces this really quite svelte code, which is now only about 300 lines!:
// src/bundle.js
var app = (function () {
'use strict';
function noop() { }
function run(fn) {
return fn();
}
function blank_object() {
return Object.create(null);
}
function run_all(fns) {
fns.forEach(run);
}
function is_function(thing) {
return typeof thing === 'function';
}
function safe_not_equal(a, b) {
return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
function append(target, node) {
target.appendChild(node);
}
function insert(target, node, anchor) {
target.insertBefore(node, anchor || null);
}
function detach(node) {
node.parentNode.removeChild(node);
}
function element(name) {
return document.createElement(name);
}
function text(data) {
return document.createTextNode(data);
}
function space() {
return text(' ');
}
function attr(node, attribute, value) {
if (value == null)
node.removeAttribute(attribute);
else if (node.getAttribute(attribute) !== value)
node.setAttribute(attribute, value);
}
function children(element) {
return Array.from(element.childNodes);
}
function set_data(text, data) {
data = '' + data;
if (text.data !== data)
text.data = data;
}
let current_component;
function set_current_component(component) {
current_component = component;
}
const dirty_components = [];
const binding_callbacks = [];
const render_callbacks = [];
const flush_callbacks = [];
const resolved_promise = Promise.resolve();
let update_scheduled = false;
function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
function add_render_callback(fn) {
render_callbacks.push(fn);
}
function flush() {
const seen_callbacks = new Set();
do {
// first, call beforeUpdate functions
// and update components
while (dirty_components.length) {
const component = dirty_components.shift();
set_current_component(component);
update(component.$$);
}
while (binding_callbacks.length)
binding_callbacks.pop()();
// then, once components are updated, call
// afterUpdate functions. This may cause
// subsequent updates...
for (let i = 0; i < render_callbacks.length; i += 1) {
const callback = render_callbacks[i];
if (!seen_callbacks.has(callback)) {
callback();
// ...so guard against infinite loops
seen_callbacks.add(callback);
}
}
render_callbacks.length = 0;
} while (dirty_components.length);
while (flush_callbacks.length) {
flush_callbacks.pop()();
}
update_scheduled = false;
}
function update($$) {
if ($$.fragment !== null) {
$$.update();
run_all($$.before_update);
$$.fragment && $$.fragment.p($$.ctx, $$.dirty);
$$.dirty = [-1];
$$.after_update.forEach(add_render_callback);
}
}
const outroing = new Set();
function transition_in(block, local) {
if (block && block.i) {
outroing.delete(block);
block.i(local);
}
}
function mount_component(component, target, anchor) {
const { fragment, on_mount, on_destroy, after_update } = component.$$;
fragment && fragment.m(target, anchor);
// onMount happens before the initial afterUpdate
add_render_callback(() => {
const new_on_destroy = on_mount.map(run).filter(is_function);
if (on_destroy) {
on_destroy.push(...new_on_destroy);
}
else {
// Edge case - component was destroyed immediately,
// most likely as a result of a binding initialising
run_all(new_on_destroy);
}
component.$$.on_mount = [];
});
after_update.forEach(add_render_callback);
}
function destroy_component(component, detaching) {
const $$ = component.$$;
if ($$.fragment !== null) {
run_all($$.on_destroy);
$$.fragment && $$.fragment.d(detaching);
// TODO null out other refs, including component.$$ (but need to
// preserve final state?)
$$.on_destroy = $$.fragment = null;
$$.ctx = [];
}
}
function make_dirty(component, i) {
if (component.$$.dirty[0] === -1) {
dirty_components.push(component);
schedule_update();
component.$$.dirty.fill(0);
}
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
}
function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) {
const parent_component = current_component;
set_current_component(component);
const prop_values = options.props || {};
const $$ = component.$$ = {
fragment: null,
ctx: null,
// state
props,
update: noop,
not_equal,
bound: blank_object(),
// lifecycle
on_mount: [],
on_destroy: [],
before_update: [],
after_update: [],
context: new Map(parent_component ? parent_component.$$.context : []),
// everything else
callbacks: blank_object(),
dirty
};
let ready = 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);
}
return ret;
})
: [];
$$.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);
}
class SvelteComponent {
$destroy() {
destroy_component(this, 1);
this.$destroy = noop;
}
$on(type, callback) {
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
callbacks.push(callback);
return () => {
const index = callbacks.indexOf(callback);
if (index !== -1)
callbacks.splice(index, 1);
};
}
$set() {
// overridden by instance, if it has props
}
}
/* src/App.svelte generated by Svelte v3.16.0 */
function create_fragment(ctx) {
let main;
let h1;
let t0;
let t1;
let t2;
let t3;
let p0;
let t4;
let t5;
let t6;
let t7;
let p1;
return {
c() {
main = element("main");
h1 = element("h1");
t0 = text("Hello ");
t1 = text(/*name*/ ctx[0]);
t2 = text("!");
t3 = space();
p0 = element("p");
t4 = text("Your lucky number is ");
t5 = text(/*number*/ ctx[1]);
t6 = text(".");
t7 = space();
p1 = element("p");
p1.innerHTML = `Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.`;
attr(h1, "class", "svelte-1tky8bj");
attr(main, "class", "svelte-1tky8bj");
},
m(target, anchor) {
insert(target, main, anchor);
append(main, h1);
append(h1, t0);
append(h1, t1);
append(h1, t2);
append(main, t3);
append(main, p0);
append(p0, t4);
append(p0, t5);
append(p0, t6);
append(main, t7);
append(main, p1);
},
p(ctx, [dirty]) {
if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]);
if (dirty & /*number*/ 2) set_data(t5, /*number*/ ctx[1]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(main);
}
};
}
function instance($$self, $$props, $$invalidate) {
let { name } = $$props;
let { number } = $$props;
$$self.$set = $$props => {
if ("name" in $$props) $$invalidate(0, name = $$props.name);
if ("number" in $$props) $$invalidate(1, number = $$props.number);
};
return [name, number];
}
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, { name: 0, number: 1 });
}
}
const app = new App({
target: document.body,
props: {
name: 'World',
number: 42
}
});
return app;
}());
//# sourceMappingURL=bundle.js.map
This file, bundle.js
, is what all of the following code blocks refer to, unless otherwise noted.
Q: Where are the elements created in create_fragment
mounted?
Here, in init
:
...
if (options.target) {
...
ππππ
mount_component(component, options.target, options.anchor);
ππππ
flush();
...
}
And mount_component
, in the first two lines of its definition, destructures fragment
from component.$$
then calls fragment.m
:
function mount_component(component, target, anchor) {
const { fragment, on_mount, on_destroy, after_update } = component.$$;
fragment && fragment.m(target, anchor);
...
As a reminder from part 1, m
stands for "mount" (it was actually called that when compiling in dev mode), and it takes the elements created in fragment.c
and adds them to the DOM with eminently readable and grokkable JS:
m(target, anchor) {
// these πππ three parameters are equivalent to `document.body`, <main/>, and `undefined`
insert(target, main, anchor);
append(main, h1);
append(h1, t0);
append(h1, t1);
append(h1, t2);
append(main, t3);
append(main, p0);
append(p0, t4);
append(p0, t5);
append(p0, t6);
append(main, t7);
append(main, p1);
},
...
function append(target, node) {
target.appendChild(node);
}
function insert(target, node, anchor) {
target.insertBefore(node, anchor || null);
}
Reminder: This is one of the things that makes Svelte great, and unique among popular frameworks: The abstractions are not so deep or complex; so far, we are never far from document.doSomething
operations.
Disappearing Questions
Now that we've left dev mode for our explorations, it seems that a few outstanding questions have evaporated:
What is __svelte_meta
for?
It doesn't appear in bundle.js
in production mode! It's for dev tooling, must be: "Error X on line Y, character Z", and so on.
Mystery solved! π΅
What is the difference between app.$set
and app.$inject_state
, if any?
Well, their implementations are identical,
// version of bundle.js in dev mode
function instance($$self, $$props, $$invalidate) {
...
$$self.$set = $$props => {
if ("name" in $$props) $$invalidate(0, name = $$props.name);
if ("number" in $$props) $$invalidate(1, number = $$props.number);
};
$$self.$inject_state = $$props => {
if ("name" in $$props) $$invalidate(0, name = $$props.name);
if ("number" in $$props) $$invalidate(1, number = $$props.number);
};
...
}
annnnnddd... $inject_state
doesn't even get defined in the production build,
// production build of bundle.js
function instance($$self, $$props, $$invalidate) {
let { name } = $$props;
let { number } = $$props;
$$self.$set = $$props => {
if ("name" in $$props) $$invalidate(0, name = $$props.name);
if ("number" in $$props) $$invalidate(1, number = $$props.number);
};
return [name, number];
}
so let's assume it's a very careful piece of naming to distinguish what dev tools use to set state, as compared with how state is set internally in the app.
Turns out, yep!:
Svelte@sveltejs@zevav That's another dev mode hook, used for hot module reloading18:53 PM - 06 Dec 2019
Finally, it looks like the setter and getter methods on App
don't appear in production mode. We can therefore scratch that question off the list, as we're not concerned with the dev tooling part of the generated code for now.
The $$invalidate
Callback
In the instance
function (πππ much shorter in prod), the $set
method is bound to app
. Presumably this is the only way prop updates propagate through the app, so it'll behoove us to understand how it's defined in the calling code:
...
function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) {
...
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);
}
return ret;
})
This is hard to read, so let's write it as a normal, somewhat simplified, non-anonymous function:
function invalidateProp(i, ret, value = ret) {
const currentVal = $$.ctx[i]
const newVal = value
if (currentVal !== newVal) {
// do some things with `$$.bound` and `ready` that will never trigger in the current app
}
$$.ctx[i] = newVal
return ret
}
So if invalidateProp
is called like so,
invalidateProp(0, name = "Whirl");
the only things that will happen are
1) $$.ctx[0]
will equal "Whirl"
2) "Whirl" will be returned.
3) Nothing anywhere will consume that return value in the current app.
Hypothesis #3: $$.bound
and ready
Will Come Into Play If There's Some Interactive UI
Furthermore, I'm thinking $$invalidate
's return value might actually get used somewhere. There's plenty more to explore ahead of this test, but let's forge ahead and see what magic the compiler has for us.
Testing Hypothesis #3
Let's make a button to increment number
:
<!-- src/App.svelte -->
<script>
export let name;
export let number;
</script>
<main>
<h1>Hello {name}!</h1>
<p>Your lucky number is {number}.</p>
<p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p>
<!-- πππ new -->
<button on:click={() => number += 1}>
Make Number More Bigger
</button>
</main>
Isn't Svelte so beautifully straightforward and succinct? πππ
Then, npm run build
. To view the app, run python3 -m http.server
from the app
folder, then navigate to http://localhost:8000. Click that beautiful button and watch your lucky number mutate!
Before hypothesis validating, bundle.js
has some new toys for us! Here are the new lines in the resulting bundle.js
:
function listen(node, event, handler, options) {
node.addEventListener(event, handler, options);
return () => node.removeEventListener(event, handler, options);
c() {
...
t11 = space();
button = element("button");
button.textContent = "Make Number More Bigger";
...
dispose = listen(button, "click", /*click_handler*/ ctx[2]);
// and m() mounts πππ them
}
d(detaching) {
if (detaching) detach(main);
dispose();
}
function instance($$self, $$props, $$invalidate) {
...
const click_handler = () => $$invalidate(0, number += 1);
...
return [number, name, click_handler];
}
Running through the changes, there's nothing shocking. The button is added, its click handler is added to the DOM, and the click handler is set up to be removed if App
is detached. click_handler
is added to $$.ctx
.
I don't see any of my hypotheses proving out here! $$.bound
remains an empty object, $$invalidate
's return value remains un-consumed, and there's still only one spot where ready
is set to true
, and that's after instance
is called.
Sidenote: There was one other change, and it's really weird:
class App extends SvelteComponent {
constructor(options) {
super();
// πππ before
// init(this, options, instance, create_fragment, safe_not_equal, { name: 0, number: 1 });
// πππ after
init(this, options, instance, create_fragment, safe_not_equal, { name: 1, number: 0 });
}
}
The props have inexplicably swapped their indices. What madness is this?! Restraining myself from tweeting at Svelte about this one... for now. Maybe it will explain itself as we go.
The Most Basic Knowledge We're Missing
I still don't know what actually triggers the function update
to run!
As a reminder, we studied update
in part 1 because it was the only place where $$.fragment
βthe object containing the methods for creating, destroying, and updating the rendered elementsβwas updated. As a further reminder, the key line in update
calls $$.fragment.p($$.ctx, $$.dirty)
, fragment.p
being short for fragment.update
: It checks the value of dirty
and potentially updates the node identified by dirty
, if the provided new value differs from the node's current one.
...
function update($$) {
if ($$.fragment !== null) {
$$.update();
run_all($$.before_update);
$$.fragment && $$.fragment.p($$.ctx, $$.dirty);
$$.dirty = [-1];
$$.after_update.forEach(add_render_callback);
}
}
...
function create_fragment(ctx) {
...
// === $$.fragment.p
p(ctx, [dirty]) {
if (dirty & /*name*/ 2) set_data(t1, /*name*/ ctx[1]);
if (dirty & /*number*/ 1) set_data(t5, /*number*/ ctx[0]);
},
...
}
Conveniently, the function update
(distinct from the method $$.fragment.p
) is called in only one place:
function flush() {
const seen_callbacks = new Set();
do {
// first, call beforeUpdate functions
// and update components
while (dirty_components.length) {
const component = dirty_components.shift();
set_current_component(component);
update(component.$$);
}
while (binding_callbacks.length)
binding_callbacks.pop()();
// then, once components are updated, call
// afterUpdate functions. This may cause
// subsequent updates...
for (let i = 0; i < render_callbacks.length; i += 1) {
const callback = render_callbacks[i];
if (!seen_callbacks.has(callback)) {
callback();
// ...so guard against infinite loops
seen_callbacks.add(callback);
}
}
render_callbacks.length = 0;
} while (dirty_components.length);
while (flush_callbacks.length) {
flush_callbacks.pop()();
}
update_scheduled = false;
}
There's a lot here, but as with much of the code generated by Svelte with a basic app, we don't have to concern ourselves with any of the callback-related parts. This is the relevant section:
...
while (dirty_components.length) {
const component = dirty_components.shift();
set_current_component(component);
update(component.$$);
...
This is the first time we're dealing with dirty_components
: Where is this array modified? Why, in the aptly named make_dirty
function.
function make_dirty(component, i) {
if (component.$$.dirty[0] === -1) {
dirty_components.push(component);
schedule_update();
component.$$.dirty.fill(0);
}
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
}
And where is make_dirty
called?
function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) {
...
let ready = 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);
}
return ret;
})
: [];
$$.update();
ready = true;
...
}
This is a bit confusing, though, because make_dirty
is only called if ready
is true
, and ready
appears to be false when the $$invalidate
callback is defined.
I have a little JS puzzle for you, Twitter:08:30 AM - 09 Dec 2019
For now we're going to skate right past the scope monster and accept the mystery of ready
's truthiness.
Thank you to my fellow Recurse Center alum Thomas Ballinger for demystifying this:
Yup, it was a scope monster, or to put it another way, "scope". To spell it out, that πππ ready = true
takes effect for the ready
inside the anonymous function's body, well before it's called as $$invalidate
.
Here's more clarification from Svelte themselves:
Svelte@sveltejs@zevav ready will be false if an invalidation happens during initialisation23:38 PM - 09 Dec 2019
π€ Methinks this will make more sense when I'm older. I mean, in part X of our little adventure...
Okay then, ready
is true for our purposes, which means when invalidateProp
is called, make_dirty
gets called with the App
instance for component
and the index of whatever prop has been modified. So, something like make_dirty(App, 1)
.
The function make_dirty
1) adds the component (App
instance here) to an array called dirty_components
2) does something confusing in schedule_update
, but, importantly, calls flush
3) updates component.$$.dirty
function make_dirty(component, i) {
...
dirty_components.push(component);
schedule_update();
component.$$.dirty.fill(0);
...
}
...
function schedule_update() {
...
resolved_promise.then(flush)
}
So we've found where dirty_components
is mutated, and as a bonus, where flush
gets called (schedule_update
).
We'll study the Array.fill
later, but returning to flush
's definition, it appears that dirty_components
is being used as a queue:
const component = dirty_components.shift();
set_current_component(component);
update(component.$$);
The meaning of current_component
doesn't seem important in this early moment when there's only a single component in our app. This makes the only interesting code here the call to update
, which as we already know triggers set_data
on any relevant elements.
The Last Piece
We nearly have the full picture of how prop changes make their way through bundle.js
. There is a curveball with respect to our current understanding, though:
What is $set
For???
app.$set
is never called in our app: It appears to be a debugging utility, or maybe a hook for external tools. So this function isn't part of the pipeline of functions handling prop changes. Sorry about that; one day the knowledge we built about $set
will have its moment, I assure you!
So Where? Sneaky Succinctness
It took me a bit of console.log
-ing to realize where the pipeline begins:
$$.ctx = instance
? instance(component, prop_values, (i, ret, value = ret) => {
// right here πππ
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if ($$.bound[i])
$$.bound[i](value);
if (ready)
make_dirty(component, i);
}
return ret;
})
: [];
It isn't clear to me at all why this anonymous function runs more than once (on App
instantiation), but it can be demonstrated that it does with
...
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = "default")) {
...
Which functions like so:
This is a bit sneaky, stuffing a variable reassignment inside a function call! Nevertheless, we now have the pipeline mapped:
Props Pipeline
1) init
: if(not_equal($$.ctx[i], $$.ctx[i] = value))
(the assignment there)
2) init
: make_dirty(component, i);
3) make_dirty
: dirty_components.push(component);
4) make_dirty
: schedule_update();
5) make_dirty
: component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
5) schedule_update
: resolved_promise.then(flush);
(equivalent to flush()
)
6) flush
: while (dirty_components.length) update(component.$$);
7) update
: $$.fragment.p($$.ctx, $$.dirty);
8) $$.fragment.p
: if (dirty & /*number*/ 1) set_data(t5, /*number*/ ctx[0]);
9) set_data
: if (text.data !== data) text.data = data;
A note about resolved_promised.then
: It isn't exactly the same as calling flush
, as even though schedule_update
is called before component.$$.dirty
is mutated, that mutation has taken place by the time flush
executes here. The mechanism for this isn't clear to me yet.
There's actually a variation on the start of this pipeline, as $$invalidate
is actually called in the click handler:
function instance($$self, $$props, $$invalidate) {
...
const click_handler = () => $$invalidate(0, number += 1);
...
}
function create_fragment(ctx) {
...
c() {
dispose = listen(button, "click", /*click_handler*/ ctx[2]);
...
}
...
}
1) click
2) instance
: const click_handler = () => $$invalidate(0, number += 1);
3) 1-9 above
Next Time
Join us next time for the further adventures of Svelte Spelunker! Discover the secrets of the runtime, such as
1) How does bundle.js
change with increasing app complexity? Multiple components, for example, or dynamically re-rendering props/state.
2) Can dirty
ever contain anything besides a single integer? In other words, are elements updated one after the next ("ATOMIC"), or can update
sometimes operate on more than one element at a run?
3) When are components and elements destroyed? Are Svelte and Rollup as efficient about unnecessary re-renders as billed?
4) Is it a security problem that the user can directly manipulate the state from a browser's JavaScript console?
New Questions
1) What's up with step 1 of the props pipeline? How and why does it run when a prop changes?
2) how do $$.bound
and ready
figure into init
?
3) This has been there all along, but why the space()
s in block
?
4) What is outroing
? An inside joke? Obscure British slang? Both? π©
5) Why does $set
even exist? Hint:
Svelte@sveltejs@zevav No, $set is non-dev API. It will differ as soon as a component has private state23:34 PM - 09 Dec 2019
5) Why does Rollup/Svelte compiler include "name" in the $set binding (in instance
) when it currently will never be mutated?
6) Why did number
and name
swap position in the call to init
when number
got attached to an event listener?
7) What's this business doing exactly? component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
Top comments (1)
Welcome to Brave new World! I really appreciate you walking these dangerous terrains of Svelte internals.