最近有一个官网页,打算用svelte体验一下,顺便学习了一下svelte(发音:[svelt]),整体来说,svelte是比较简洁的,上手很快。不过与其说是一个前端框架,不如说是一个“dom操作编译器”。svelte的开发代码,在编译阶段会被编译成一系列的dom操作的代码,运行时的代码很少。因此svelte.js的体积很小(只保留了脏值检测更新和封装dom操作API等core代码)。本文从一下几个方面聊一聊对于svelte的认识。
- svelte初体验
- svelte的语法
- Virtual Dom和Dom
- 优缺点
- svelte源码阅读
原文地址在我的博客: github.com/fortheallli…
一、svelte初体验
我们直接来看官网的例子:
实现的功能也很简单,就是两个Input的值求和,然后展示出来。用svelte编写的代码为:
<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>
上述代码很简洁,像vue一样也是类似style dom script的三段式写法,不过比vue更加简洁一点,比如dom不需要template包裹等等。
同样的上述的例子的代码如果用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>
);
}
上述react的写法,必须要先弄懂useState的含义等,此外缺少了默认的双向数据绑定,代码有一点冗余。
同样的上述的例子的代码如果用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>
三者对比:
框架名称 | svelte | react | vue | demo字符数 | 145 | 445 | 263 |
---|
单纯的说,svelte编码只需要145个字符,比vue和react少,因此得出说svelte的编码体积更小,这样是不对的,因为svelte会在编译阶段将代码编译到更加贴近dom操作的代码,上述例子的代码,编译后的结果为:
/* 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();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
export default App;
在编译后生成的代码其实代码量也不小,是远远大于145个字符的,也不能说因为编译后的代码量大,所以说svelte有点名不副实,并不能减少运行时代码的体积。要考虑到svelte的运行时代码是很少的.我们来对比一下:
框架名称 | react | vue | angular | svelte | 体积 | 42k | 22k | 89.5k | 1.6k |
---|
从上述对比中可以看出,svelte的体积很少,虽然其业务代码在编译后会生产较多的代码。得益于较少的运行时代码。虽然svelte代码的随着业务的编写增量速度比较快,得益于其很小的包体积1.6k,对于一般中小型项目而言,整体运行的代码(编译后的代码+包体积)还是比较小的,所以可以说svelte项目的代码较小。不过对于大型项目而言,因为svelte随着业务的进行,运行时代码增量陡峭,大型项目体积并不会比react、vue等小,因此需要辩证看待。
此外虽说svelte的代码在编译后体积很大,但是在编译前的代码,其实很简洁,这种简洁,一定程度上,可以增强开发体验。
二、 svelte的语法
svelte的写法跟vue有点类似,是指令式和响应式的。
-
基本用法
<script>
let name = 'world';
</script>
<h1>Hello {name}!</h1>
<style>
h1{
color:red
}
</style>
这是一个最简单的hello world的例子,上述代码中很简洁。在编译后的代码分为js编译和css编译。
- js编译
/* 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();
init(this, options, null, create_fragment, safe_not_equal, {});
}
}
export default App;
svelte/internal包中是一些封装了dom操作的函数。
-
css编译结果:
h1.svelte-khrn1o{color:red}
css是通过创建style标签引入到最后的dom中的。
-
指令形式和数据绑定
<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>
还是以上面的例子为例,上述就是一个指令形式+数据绑定的形式。跟vue的写法很相似,改例子绑定了input和a, input和b.效果如下:
这里的$total: 就是reactive statement. 类似vue中的计算属性。
-
组件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>
在svelte中的组件的compose也是跟react中类似的,不同的是在react中export的属性就是组件的props,写法上比较简洁,此外,export const 和export function、export class这3个组件的props是只读的,不可写。
-
模版语法
在svelte中,html相关的场景适用于模版语法,最简单的模版语法为:
{#if answer === 42} <p>what was the question?</p> {/if}
这里介绍几个在svelte中几个比较有趣的模版语法。
- @debug
<script>
export let name = "yuxl"
</script>
{@debug name}
<span>
{name}
</span>
运行debugger的结果为: @debug 在后面跟的参数name发生变化的时候会进行debugger,从上图我们看到debugger的地方上下文的代码是编译后运行时,跟编码的时候有一点区别,也进一步说明,svelte可以看作是一个前端的编译框架,真正运行时的代码是编译后的结果。
- @html
<script lang='typescript'>
export let name = "yuxl"
const age = '<span>20</span>'
</script>
<div>
<span>{name}</span>
{@html age}
</div>
-
#await 用法为:{#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>
-
动画效果
在svelte中,对于原始的dom元素,自带了一些动画指令,在一般的官网或者活动页中,场景最多的就是动画效果,svelte自带的动画指令,因此在写官网的时候方便了不少。
以transition:fly为例:
<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}
最后的结果为:
当然在svelte中也支持自定义动画指令。
-
组件的生命周期
svelte组件也提供了完整的生命周期。onMount、beforeUpdate
、afterUpdate
、onDestroy
等。见名思意,这里不一一介绍,跟react & vue的组件生命周期近似。
除了上述之外,svelte还支持自定义元素(custom element), store以及context等等。
三、Virtual Dom和Dom
这个其实可以,比较客观的去看待,svelte的作者认为,Virtual Dom的性能并没有太大的问题,不管是diff算法还是render的过程都没有什么性能问题,不过作者认为,svelte不需要diff,还是有一点优势的。虽然diff很快,但是没有diff的话,显然会更快的得到渲染结果。
svelte的编译后的结果来看,所有的dom的变动都变为了直接的dom操作行为,是不需要做diff的,这种方法,没有diff/patch,因此从速度来看,肯定更快一些。 比如:
<script>
import { fade } from "svelte/transition";
let visible = false
function handleClick(){
visible = true
}
</script>
<div>
<div on:click={handleClick}>点击</div>
{#if visible}
<div transition:fade ="{{ duration: 2000 }}" >
fades in and out
</div>
{/if}
</div>
上述这个例子中,修改了visible,编译后的代码知道这个行为,这是一个确定的会如何影响dom的行为,编译后的结果部分为:
可以看到,state的改变如何影响dom在svelte的编译结果中都是很确定的。
除了性能问题,svelte的作者认为,因为virtualDom的存在,需要保存new object和old object的虚拟dom对象,在react的编程中,每一次渲染都有这两个对象,这两个对象,在正常的开发中,很容易添加一些冗余代码:
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>
);
}
在这个例子中,为每一个li都绑定了一个事件,这是不过度优化情况下的正常下发,因为virtualDom虚拟dom的存在,每一次state更新的时候,每一个new object和old object都包含了每一个li的绑定函数,这些是冗余的代码,增加了代码的体积等。
四、优缺点
个人归纳了一下几个优缺点:
-
优点:
- 体积很小,是真的小,包体积只有1.6k,对于小型项目比如官网首页,活动页等确实可以拿来试试。上手也很快,很轻量级。类似活动页这种简单页面的lowcoder系统,也可以尝试一下,因为框架本身提供的api,应该是目前前端框架里面最简单的。
- no virtual dom的形式一定程度上确实要快一些,没有了diff/path
-
缺点
- 虽然包的体积小,但是编译后的代码其实并不小,代码总量的增加曲线其实还是有一定陡峭的。在大型项目中没有证明自己。
- 生态问题,生态其实并不是很完善,虽然类似的比如组件库之类的都有,但是没有很完善。
五、源码阅读
首先svelte的源码分为两部分,compiler和runtime,compiler主要的作用是将开发代码编译成运行时的代码,具体如何编译不是本文所要关注的代码。本文主要关注的是编译后的运行时的代码runtime。
-
dom操作相关core api
我们以最简单的hello world为例: svelte编译前源码:
<h1>Hello world!</h1>
svelte编译后的代码:
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();
init(this, options, null, create_fragment, safe_not_equal, {});
}
}
export default App;
这里的App就可以直接使用了,比如渲染到一个父dom中可以这样使用:
import App from './App.svelte'
var app = new App({
target: document.body,
});
export default app;
上述方法就可以把App这个编译后的运行时组件渲染到body中,我们来看编译后的代码。
- create_fragment
在svelte组件中,与dom相关的部分封装在了create_fragment中,该函数创建了一个Fragment, 该函数返回一个包含dom操作的对象:
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;
}
在上述的例子中,c对应创建一个子dom元素,m表示创建元素要渲染元素时需要执行的函数,d表示删除元素时的操作。上述的例子中:
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);
}
};
}
在m中的intert和d中的detach方法,都是原生的dom操作方法,上述Fragment的意思是创建了h1这个dom,并在渲染的时候插入到目标dom节点中,在Fragment这个组件元素被销毁的时候,销毁被创建的子dom元素 h1。
element、insert、detach等方法都是原生的dom操作,具体源码如下所示:
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);
}
- 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;
}
}
}
SvelteComponent组件定义了如何销毁组件以及如何设置组件的属性,以及如何增加监听函数,其中最重要的是定义了组件的实例属性 .
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[];
}
发现SvelteComponent组件确实包含了ctx上下文内容,以及组件的生命周期属性,以及组件的脏值检测等相关的属性。
-
init函数 `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); }
init函数在SvelteComponent组件内部调用,用于实例属性的初始化。这里最重要的是$$.ctx的赋值部分,后续会用来做脏值检测。ctx中保存了所有的再多次渲染中都存在的值,包含了内部的state以及监听处理函数等等。
-
脏值检测和更新部分
-
这里我们以一个带有鼠标时间的svelte组件为例,
编译前的代码:
<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>
svelte编译后的代码与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];
}
这里多了一个instance函数,而这个instance函数在svelteComponent的init函数中就是用作脏值检测和更新的。
$$.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;
})
: [];
如果值发生了变动,就触发make_dirty函数:
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));
}
make_dirty标记了哪一些脏组件,然后对脏组件执行schedule_update方法来更新组件:
export function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
schedule_update在需要更新时候,在下一个微任务重执行flush:
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);
}
简化后的flush方法如上所示,就是遍历整个脏组件,执行所有的脏组件中的更新方法update.update方法的定义为:
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);
}
}
update方法标记自身组件为脏,并且制定自身组件fragment中的p(全名:update)也就是前面的fragment中的:
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);
},
在p方法中,直接操作dom改变UI。
总结来看,组件更新的步骤为以下几步:
- 事件或者其他操作出发更新流程
- 在instance的$$invalidate方法中,比较操作前后ctx中的值有没有发生改变,如果发生改变则继续往下
- 执行make_dirty函数标记为脏值,添加带有脏值需要更新的组件,从而继续触发更新
- 执行schedule_update函数
- 执行flush函数,将所有的脏值组件取出,以此执行其update方法
- 在update方法中,执行的是Fragment自身的p方法,p方法做的事情就是确定需要更新组件,并操作和更新dom组件,从而完成了最后的流程
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!