Recently, I have an official website, I plan to use svelte to experience, by the way, I learned about svelte(pronunciation: [svelt]). Overall, svelte is relatively simple and easy to use. However, it is more of a “DOM manipulation compiler” than a front-end framework. Svelte’s development code is compiled into a series of DOM operations at compile time, with very little code at run time. As a result, svelte.js is small in size (keeping only core code such as dirty value detection updates and encapsulating DOM manipulation apis). This article will talk about the understanding of SvelTE from the following aspects.
- At the beginning of svelte experience
- The grammar of the svelte
- Virtual Dom and Dom
- The advantages and disadvantages
- Svelte source reading
The original address is on my blog: github.com/fortheallli…
First, svelte experience
Let’s go straight to the website example:
The implementation is simple: sum the values of the two inputs and display them. The code written with Svelte is:
<script>
let a = 1;
let b = 2;
</script>
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {a + b}</p>
Copy the code
The above code is very simple, like vue, it is similar to the style dom script, but a little more concise than vue, such as DOM does not need a template wrapper, etc.
The same code for the above example is written with react:
import React, { useState } from 'react';
export default() = > {const [a, setA] = useState(1);
const [b, setB] = useState(2);
function handleChangeA(event) { setA(+event.target.value); }
function handleChangeB(event) { setB(+event.target.value); }
return (
<div>
<input type="number" value={a} onChange={handleChangeA}/>
<input type="number" value={b} onChange={handleChangeB}/>
<p>{a} + {b} = {a + b}</p>
</div>
);
}
Copy the code
In addition to the lack of default two-way data binding, the code is a little redundant.
The same code for the above example is written in vue:
<template>
<div>
<input type="number" v-model.number="a">
<input type="number" v-model.number="b">
<p>{{a}} + {{b}} = {{a + b}}</p>
</div>
</template>
<script>
export default {
data: function() {
return { a: 1.b: 2}; }};</script>
Copy the code
Comparison of the three:
Name of the framework | svelte | react | vue |
---|---|---|---|
The demo characters | 145 | 445 | 263 |
Svelte code requires 145 characters, which is less than vue and React, so it is incorrect to say that svelte code is smaller than vue and React. This is because svelte will compile the code into code that is more closely related to DOM operations.
/* App.svelte generated by Svelte v3.38.3 */
import {
SvelteComponent,
append,
attr,
detach,
element,
init,
insert,
listen,
noop,
run_all,
safe_not_equal,
set_data,
set_input_value,
space,
text,
to_number
} from "svelte/internal";
function create_fragment(ctx) {
let input0;
let t0;
let input1;
let t1;
let p;
let t2;
let t3;
let t4;
let t5;
let t6_value = /*a*/ ctx[0] + /*b*/ ctx[1] + "";
let t6;
let mounted;
let dispose;
return {
c() {
input0 = element("input");
t0 = space();
input1 = element("input");
t1 = space();
p = element("p");
t2 = text(/*a*/ ctx[0]);
t3 = text("+");
t4 = text(/*b*/ ctx[1]);
t5 = text("=");
t6 = text(t6_value);
attr(input0, "type"."number");
attr(input1, "type"."number");
},
m(target, anchor) {
insert(target, input0, anchor);
set_input_value(input0, /*a*/ ctx[0]);
insert(target, t0, anchor);
insert(target, input1, anchor);
set_input_value(input1, /*b*/ ctx[1]);
insert(target, t1, anchor);
insert(target, p, anchor);
append(p, t2);
append(p, t3);
append(p, t4);
append(p, t5);
append(p, t6);
if(! mounted) { dispose = [ listen(input0,"input"./*input0_input_handler*/ ctx[2]),
listen(input1, "input"./*input1_input_handler*/ ctx[3])]; mounted =true; }},p(ctx, [dirty]) {
if (dirty & /*a*/ 1&& to_number(input0.value) ! = =/*a*/ ctx[0]) {
set_input_value(input0, /*a*/ ctx[0]);
}
if (dirty & /*b*/ 2&& to_number(input1.value) ! = =/*b*/ ctx[1]) {
set_input_value(input1, /*b*/ ctx[1]);
}
if (dirty & /*a*/ 1) set_data(t2, /*a*/ ctx[0]);
if (dirty & /*b*/ 2) set_data(t4, /*b*/ ctx[1]);
if (dirty & /*a, b*/ 3&& t6_value ! == (t6_value =/*a*/ ctx[0] + /*b*/ ctx[1] + "")) set_data(t6, t6_value);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(input0);
if (detaching) detach(t0);
if (detaching) detach(input1);
if (detaching) detach(t1);
if (detaching) detach(p);
mounted = false; run_all(dispose); }}; }function instance($$self, $$props, $$invalidate) {
let a = 1;
let b = 2;
function input0_input_handler() {
a = to_number(this.value);
$$invalidate(0, a);
}
function input1_input_handler() {
b = to_number(this.value);
$$invalidate(1, b);
}
return [a, b, input0_input_handler, input1_input_handler];
}
class App extends SvelteComponent {
constructor(options) {
super(a); init(this, options, instance, create_fragment, safe_not_equal, {}); }}export default App;
Copy the code
The amount of code generated after compilation is actually a lot more than 145 characters, and svelte does not live up to its name because of the amount of compiled code. It does not reduce the size of the runtime code. Consider that the runtime code for svelTE is minimal. Let’s compare:
Name of the framework | react | vue | angular | svelte |
---|---|---|---|---|
volume | 42k | 22k | 89.5 k. | 1.6 k. |
As you can see from the above comparison, SvelTE has a small volume, although its business code produces more code when compiled. Benefits from less runtime code. Although svelTE code is written in increments with the business faster, thanks to its small package size of 1.6K, the overall running code (compiled code + package size) is relatively small for a typical small to medium size project, so svelTE projects are small. However, for large projects, because svelTE’s code increment is steep as business goes on, the volume of large projects is not smaller than react, Vue, etc., so we need to treat them dialectically.
In addition, although svelte code is large after compilation, the code before compilation is actually very concise, which, to some extent, enhances the development experience.
The grammar of svelte
Svelte is written in a similar way to vue. It is both directive and responsive.
-
Basic usage
<script>
let name = 'world';
</script>
<h1>Hello {name}!</h1>
<style>
h1{
color:red
}
</style>
Copy the code
This is the simplest hello World example, very concise in the above code. The compiled code is divided into JS compilation and CSS compilation.
- Js compilation
/* App.svelte generated by Svelte v3.38.3 */
import {
SvelteComponent,
attr,
detach,
element,
init,
insert,
noop,
safe_not_equal
} from "svelte/internal";
function create_fragment(ctx) {
let h1;
return {
c() {
h1 = element("h1");
h1.textContent = `Hello ${name}! `;
attr(h1, "class"."svelte-khrn1o");
},
m(target, anchor) {
insert(target, h1, anchor);
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if(detaching) detach(h1); }}; }let name = "world";
class App extends SvelteComponent {
constructor(options) {
super(a); init(this, options, null, create_fragment, safe_not_equal, {}); }}export default App;
Copy the code
The svelte/ Internal package contains functions that encapsulate DOM operations.
-
CSS compilation results:
h1.svelte-khrn1o{color:red} Copy the code
CSS is introduced into the final DOM by creating the style tag.
-
Directive form and data binding
<script>
let a = 1;
let b = 2;
$: total = a+b
</script>
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {total}</p>
Copy the code
As in the above example, this is an instruction form + data binding form. Similar to vue, the example binds input to a, input to b. The effect is as follows:
Reactive statement. Reactive statement. Reactive statement. Similar to the computed attribute in VUE.
-
Components compose
//Name.svelte
<script lang='typescript'>
export let name = "yuxl"
</script>
<span>
{name}
</span>
//Age.svelte
<script lang='typescript'>
export let age = 18
</script>
<span>
{age}
</span>
//index.svelte
<script>
import Name from './Name.svelte'
import Age from './Age.svelte'
</script>
<div>
<Name name="some name"/>
<Age age = {20} />
</div>
Copy the code
In react, the component’s export attribute is, for example, props. In React, the component’s export attribute is, for example, props. Export const, export Function, and export Class props are read-only and cannot be written.
-
Template syntax
In Svelte, HTML-related scenarios apply to template syntax. The simplest template syntax is:
{#if answer === 42} <p>what was the question? </p> {/if}
Copy the code
Here are some of the more interesting template syntax in Svelte.
- @debug
<script>
export let name = "yuxl"
</script>
{@debug name}
<span>
{name}
</span>
Copy the code
The result of running debugger is:@debug makes debugger when the following parameter name changes. From the figure above, we can see that the context of the code in the debugger is run after compilation, which is a little different from coding. This further demonstrates that svelte can be viewed as a front-end compilation framework. The actual runtime code is the compiled result.
- @html
<script lang='typescript'>
export let name = "yuxl"
const age = '<span>20</span>'
</script>
<div>
<span>{name}</span>
{@html age}
</div>
Copy the code
-
{#await expression}… {:then name}… {:catch name}… {/await}
<script lang='typescript'> const promise = new Promise((resolve) = >{ setTimeout(() = >{ resolve("success")},2000) }) </script> <div> {#await promise} <! -- promise is pending --> <p>waiting for the promise to resolve...</p> {:then value} <! -- promise was fulfilled --> <p>The value is {value}</p> {/await} </div> Copy the code
-
Animation effects
In Svelte, for the original DOM elements, there are some animation instructions. In the general official website or active page, the scene is the most animation effect, svelte animation instructions, so when writing the official website is a lot of convenience.
Take transition:fly for example:
<script>
import { fly } from 'svelte/transition';
let visible = true;
</script>
<label>
<input type="checkbox" bind:checked={visible}>
visible
</label>
{#if visible}
<p transition:fly="{{ y: 200, duration: 2000 }}">
Flies in and out
</p>
{/if}
Copy the code
The final result is:
There is also support for custom animation instructions in Svelte.
-
The lifecycle of the component
Svelte components also provide a full lifecycle. Such as onMount, beforeUpdate, afterUpdate, and onDestroy. The component life cycle is similar to that of React & Vue.
In addition to the above, Svelte supports custom Elements, stores, contexts, and more.
Virtual Dom and Dom
In fact, this can be viewed objectively. The author of Svelte believes that the performance of Virtual Dom is not too much of a problem, and neither diff algorithm nor Render process has any performance problems. However, the author believes that Svelte does not need diff, so it still has some advantages. Although diff is fast, rendering results are obviously faster without diff.
The result of svelte compilation shows that all dom changes are direct dom operations, and there is no diff. This method has no diff/patch, so it is certainly faster. Such as:
<script>
import { fade } from "svelte/transition";
let visible = false
function handleClick(){
visible = true
}
</script>
<div>
<div on:click={handleClick}>Click on the</div>
{#if visible}
<div transition:fade ="{{ duration: 2000 }}" >
fades in and out
</div>
{/if}
</div>
Copy the code
In the example above, visible is changed so that the compiled code knows this behavior, which is a definite behavior that affects the DOM. The compiled part of the result is:
As you can see, how a change in state affects the DOM is determined in the svelTE compilation results.
In addition to performance problems, the authors of svelte believe that because of the existence of virtualDom, virtualDom objects of new object and old object need to be saved. In react programming, these two objects are present in every rendering. In normal development, it is easy to add some redundant code to these two objects:
function MoreRealisticComponent(props) {
const [selected, setSelected] = useState(null);
return (
<div>
<p>Selected {selected ? selected.name : 'nothing'}</p>
<ul>
{props.items.map(item =>
<li>
<button onClick={()= > setSelected(item)}>
{item.name}
</button>
</li>
)}
</ul>
</div>
);
}
Copy the code
In this example, each LI is bound to an event, which is normally delivered without excessive optimization. Due to the existence of virtualDom, each new Object and old Object contain the binding function of each LI in each state update. These are redundant code, increasing the volume of the code, etc.
Four, advantages and disadvantages
Personally summed up a few advantages and disadvantages:
-
Advantages:
- The volume is small, really small. The package size is only 1.6K. For small projects, such as the homepage of the official website and the activity page, it can be used to try. It’s quick to pick up and lightweight. Lowcoder system for simple pages like active pages can also be tried, because the API provided by the framework itself is probably the simplest of the front-end frameworks.
- The no virtual DOM form is somewhat faster, with no diff/path
-
disadvantages
- While the package size is small, the compiled code is not, and the increase in the amount of code is actually quite steep. Did not prove himself in large projects.
- Ecology problem, ecology is actually not very perfect, although similar such as component library and so on, but not very perfect.
Five, source reading
Firstly, the source code of SVELTE is divided into two parts, compiler and Runtime. The main function of compiler is to compile the development code into runtime code, and how to compile the code is not the focus of this article. This article focuses on the compiled runtime code, Runtime.
-
Core API for DOM manipulation
Let’s take the simplest hello World example:
<h1>Hello world! </h1>Copy the code
Svelte compiled code:
import {
SvelteComponent,
detach,
element,
init,
insert,
noop,
safe_not_equal
} from "svelte/internal";
function create_fragment(ctx) {
let h1;
return {
c() {
h1 = element("h1");
h1.textContent = "Hello world!";
},
m(target, anchor) {
insert(target, h1, anchor);
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if(detaching) detach(h1); }}; }class App extends SvelteComponent {
constructor(options) {
super(a); init(this, options, null, create_fragment, safe_not_equal, {}); }}export default App;
Copy the code
Here the App can be used directly, such as rendering to a parent DOM can be used like this:
import App from './App.svelte'
var app = new App({
target: document.body,
});
export default app;
Copy the code
The above method renders the compiled runtime component of the App into the body. Let’s look at the compiled code.
- create_fragment
The DOM-related parts of the svelte component are encapsulated in the create_fragment function, which creates a Fragment that returns an object containing DOM operations:
export interface Fragment {
key: string|null;
first: null;
/* create */ c: () = > void;
/* claim */ l: (nodes: any) = > void;
/* hydrate */ h: () = > void;
/* mount */ m: (target: HTMLElement, anchor: any) = > void;
/* update */ p: (ctx: any, dirty: any) = > void;
/* measure */ r: () = > void;
/* fix */ f: () = > void;
/* animate */ a: () = > void;
/* intro */ i: (local: any) = > void;
/* outro */ o: (local: any) = > void;
/* destroy */ d: (detaching: 0|1) = > void;
}
Copy the code
In the above example, c corresponds to the creation of a child DOM element, m represents the function to be performed when creating the element to render the element, and d represents the operation to delete the element. In the example above:
function create_fragment(ctx) {
let h1;
return {
c() {
h1 = element("h1");
h1.textContent = "Hello world!";
},
m(target, anchor) {
insert(target, h1, anchor);
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if(detaching) detach(h1); }}; }Copy the code
Intert (m) and detach (D) are both native DOM operations. The Fragment creates the DOM h1 and inserts it into the target DOM node during the Fragment process. When the Fragment component element is destroyed, the child DOM element h1 is destroyed.
Element, INSERT, detach and other methods are native DOM operations, as shown in the source code below:
export function element<K extends keyof HTMLElementTagNameMap> (name: K) {
return document.createElement<K>(name);
}
export function insert(target: NodeEx, node: NodeEx, anchor? : NodeEx) {
target.insertBefore(node, anchor || null);
}
export function detach(node: Node) {
node.parentNode.removeChild(node);
}
Copy the code
- SvelteComponent
export class SvelteComponent {
$$: T$$; $$set? :($$props: any) = > void;
$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($$props) {
if (this.$$set && ! is_empty($$props)) {this.$$.skip_bound = true;
this.$$set($$props);
this.$$.skip_bound = false; }}}Copy the code
The SvelteComponent component defines how to destroy the component, how to set its properties, how to add listening functions, and most importantly, how to define the instance properties of the component.
interface T$$ {
dirty: number[];
ctx: null|any;
bound: any;
update: () = > void;
callbacks: any;
after_update: any[];
props: Record<string, 0 | string>;
fragment: null|false|Fragment;
not_equal: any;
before_update: any[];
context: Map<any, any>;
on_mount: any[];
on_destroy: any[];
skip_bound: boolean;
on_disconnect: any[];
}
Copy the code
It is found that the SvelteComponent component does contain the CTX context content, as well as the component’s lifecycle properties, as well as the component’s dirty value detection and other related properties.
-
Js export function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) {
const parent_component = current_component; set_current_component(component); const $$: T$$ = component.$$ = { fragment: null.ctx: null.// state props, update: noop, not_equal, bound: blank_object(), // lifecycle on_mount: [].on_destroy: [].on_disconnect: [].before_update: [].after_update: [].context: new Map(parent_component ? parent_component.$$.context : options.context || []), // everything else callbacks: blank_object(), dirty, skip_bound: false }; let ready = false; $$.ctx = instance ? instance(component, options.props || {}, (i, ret, ... rest) = > { const value = rest.length ? rest[0] : ret; if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) { if(! $$.skip_bound && $$.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) { flush(); } set_current_component(parent_component); } Copy the code
The init function is called inside the SvelteComponent component to initialize instance properties. The most important part here is the assignment part of $$. CTX, which will be used for dirty value detection. CTX holds all values that will be present in multiple renderings, including internal state and listener handlers, etc.
-
Dirty value detection and update section
-
Here’s an example of a SvelTE component with mouse time,
Precompiled code:
<script>
let m = { x: 0.y: 0 };
function handleMousemove(event) {
m.x = event.clientX;
m.y = event.clientY;
}
</script>
<div on:mousemove={handleMousemove}>
The mouse position is {m.x} x {m.y}
</div>
Copy the code
Svelte compiled code adds code compared to Hello World:
function create_fragment(ctx) {
let div;
let t0;
let t1_value = /*m*/ ctx[0].x + "";
let t1;
let t2;
let t3_value = /*m*/ ctx[0].y + "";
let t3;
return{...p(ctx, [dirty]) {
if (dirty & /*m*/ 1&& t1_value ! == (t1_value =/*m*/ ctx[0].x + "")) set_data(t1, t1_value);
if (dirty & /*m*/ 1&& t3_value ! == (t3_value =/*m*/ ctx[0].y + "")) set_data(t3, t3_value); },... }; }function instance($$self, $$props, $$invalidate) {
let m = { x: 0.y: 0 };
function handleMousemove(event) {
$$invalidate(0, m.x = event.clientX, m);
$$invalidate(0, m.y = event.clientY, m);
}
return [m, handleMousemove];
}
Copy the code
There is an additional instance function, which is used to detect and update dirty values in svelteComponent’s init function.
$$.ctx = instance
? instance(component, options.props || {}, (i, ret, ... rest) = > {
const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if(! $$.skip_bound && $$.bound[i]) $$.bound[i](value);if (ready) make_dirty(component, i);
}
return ret;
})
: [];
Copy the code
If the value changes, the make_dirty function fires:
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));
}
Copy the code
Make_dirty specifies which components are dirty, and then executes the schedule_update method on dirty components to update them:
export function schedule_update() {
if(! update_scheduled) { update_scheduled =true; resolved_promise.then(flush); }}Copy the code
Schedule_update reexecutes Flush on the next microtask when an update is needed:
export function flush() {
if (flushing) return;
flushing = true;
do {
// first, call beforeUpdate functions
// and update components
for (let i = 0; i < dirty_components.length; i += 1) {
const component = dirty_components[i];
set_current_component(component);
update(component.$$);
}
set_current_component(null); . render_callbacks.length =0;
} while (dirty_components.length);
}
Copy the code
The simplified flush method is shown above, which traverses the entire dirty component and executes all the dirty components. The update.update method is defined as:
function update($$) {
if($$.fragment ! = =null) {
$$.update();
run_all($$.before_update);
const dirty = $$.dirty;
$$.dirty = [-1]; $$.fragment && $$.fragment.p($$.ctx, dirty); $$.after_update.forEach(add_render_callback); }}Copy the code
The update method flags its component as dirty and sets p in the fragment (full name: update).
p(ctx, [dirty]) {
if (dirty & /*m*/ 1&& t1_value ! == (t1_value =/*m*/ ctx[0].x + "")) set_data(t1, t1_value);
if (dirty & /*m*/ 1&& t3_value ! == (t3_value =/*m*/ ctx[0].y + "")) set_data(t3, t3_value);
},
Copy the code
In the P method, direct manipulation of the DOM changes the UI.
In summary, the steps for component update are as follows:
- Event or other action begins the update process
- In the $$invalidate method of instance, compare whether the value of CTX has changed before and after the operation. If it has changed, proceed to the next step
- Execute the make_dirty function to mark the dirty value and add components with dirty values that need to be updated, thus continuing to trigger updates
- The schedule_update function is executed
- Perform its update method by flushing all dirty value components out
- In the update method, you execute the Fragment’s own P method, which determines the component that needs to be updated, and then manipulates and updates the DOM component to complete the final process