为什么使用 Svelte (上)

背景

自2019年Svelte框架横空出世,短短两年时间就迅速占据开发者满意度、开发者兴趣度榜首,
同时市场份额稳步提升,仅次于React、Angular、Vue排至第四位。
并且作为一个比较年轻的框架,已经拥有和Vue几乎等同数量的贡献者员。

可行性报告

  • 开发者满意度

2020数据统计

img

  • 开发者兴趣度

img

  • 市场占有率

img

  • 性能

Svelte 性能整体表现优于 Vue 和 React。在操作大规模 DOM 节点时,性能表现优于基于虚拟 DOM 的 Vue 和 React。 经过 Svelte 编译过的代码体积小,所以在首屏加载时有明显的优势。

类目 Svelte Vue React
创建 1000 行表格 132.3 ms 162.3 ms 165.7 ms
创建 10000 行表格 1,212 ms 1,252 ms 1,594.9 ms
在 1000 行表格中交换两行 51.8 ms 66.6 ms 429.6 ms
首屏加载时间 19.5 ms 59.6 ms 55.6 ms

这个网站统计了现实世界中 Web 应用使用多种 Web 框架构建的相关数据。根据统计,使用一些流行框架的应用经过 gzip 压缩后的大小(KB)分别为:

Angular + ngrx:134

React + Redux:193

Vue:41.8

Svelte:9.7

为什么要用Svelte

  1. svelte是Web 应用程序编译器
  2. Svelte 无需使用虚拟 DOM 就可以提供反应性
  3. Svelte 大大简化了组件和应用程序状态管理
  4. Svelte 为可访问性问题提供了运行时警告
  5. Svelte Native 支持开发移动应用程序

编译型语言在程序执行之前,有一个单独的编译过程,将程序翻译成机器语言,以后执行这个程序的时候,就不用再进行翻译了。
解释型语言,是在运行的时候将程序翻译成机器语言,所以运行速度相对于编译型语言要慢。

在项目中哪里能用到?

  • 在通天塔中
1
2
3
4
5
6
7
8
9
const result = {{{boardParams}}}
const boardParams = JSON.parse(result)
if(!window.boardParamsConfig) {
window.boardParamsConfig = []
}
const allTarget = document.querySelectorAll('.use-svelte-target')
const target = allTarget && allTarget[allTarget.length -1]
boardParams.target = target
window.boardParamsConfig.push(boardParams)

测试Svelte

  • 在Vue项目中
1
2
3
4
5
6
7
8
9
import '../assets/bundle'

export default {
name: 'HelloWorld',
mounted() {
const target = this.$el
window.svelteMethods.init({target})
}
}
  • 在React项目中
1
2
3
4
5
6
const refSvelte = useRef(null)

useEffect(() => {
const target = refSvelte.current
window.svelteMethods.init({target})
}, [])

从这里开始

准备工作

  1. clone项目

    npx degit sveltejs/template my-svelte-project

或直接下载zip文件 下载地址

  1. 使用TS

    node scripts/setupTypeScript.js

  2. 引入router

    npm install svelte-routing –save

1
2
3
4
5
6
7
8
9
import App from './App.svelte'

const app = new App({
target: document.getElementById('app'),
// + hydrate: true
+ hydratable: true
})

export default app
  1. 创建路由
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <script lang="ts">
    import { Router, Link, Route } from "svelte-routing";
    import Home from "./routes/Home.svelte";

    export let url: string = "";
    </script>

    <Router url="{url}">
    <nav>
    <Link to="/">Home</Link>
    </nav>
    <div>
    <Route path="/"><Home /></Route>
    </div>
    </Router>

此时就可以开始我们的Svelte之旅了,官网为初学者提供了极度舒适的学习曲线,方便我们学习,同时也可以通过官方示例清晰的了解框架内部运作

走进Svelte

打开svelte源码 我们发现其是由两大部分组成,compiler 和 runtime。

先来看一个例子

1
<h1>Hello world!</h1>

会被编译为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* App.svelte generated by Svelte v3.42.4 */
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;

编译出来的结果就是有一个初始化函数,叫 create_fragment,
它是用于 dom 的初始挂载。它使用了 element 函数,通过查阅源码src/runtime/internal/dom,
我们知道它的作用就是用来创建 h1 标签实例,并且填入可变内容。除了element之外,还有space,text,svg_element等
都是用于生成真实 dom,分别是对空格,纯文本,svg 进行生成处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function element<K extends keyof HTMLElementTagNameMap>(name: K) {
return document.createElement<K>(name);
}
export function text(data: string) {
return document.createTextNode(data);
}

export function space() {
return text(' ');
}

export function empty() {
return text('');
}

create_fragment 的过程还包含有c,m,p,i,o,d等特殊名称的函数,这些函数并非编译混淆,而是 Fragment 内部的生命周期缩写。
Fragment 指得是真实 dom 的节点,它拥有着独立的生命周期和属性。源码中src/runtime/internal/Component介绍了它的定义,
它是一个真实的 dom 元素集合,它的属性并非组件属性(如下方 ts 类型定义),
分别包含了create, claim, hydrate, mount, update, mesure, fix, animate, intro, outro, destory,组件的真实变化会影响 Fragment 的变化,
Fragment 的变化影响真实的 dom,从上面例子看在 create 的过程中它创建了 h1 标签,在 mount 的过程将刚才创建的 h1 挂载到页面中,
在 update 的过程没有任何操作任何操作只有回调钩子,在 detach 的过程销毁该 Fragment。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

再来看一个例子

Nested.svelte

1
<p>This is another paragraph.</p>

App.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
import Nested from './Nested.svelte';
</script>
<style>
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>

<p>This is a paragraph.</p>
<Nested/>

会被编译为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/* App.svelte generated by Svelte v3.42.4 */
import {
SvelteComponent,
attr,
create_component,
destroy_component,
detach,
element,
init,
insert,
mount_component,
noop,
safe_not_equal,
space,
transition_in,
transition_out
} from "svelte/internal";

import Nested from './Nested.svelte';

function create_fragment(ctx) {
let p;
let t1;
let nested;
let current;
nested = new Nested({});

return {
c() {
p = element("p");
p.textContent = "This is a paragraph.";
t1 = space();
create_component(nested.$$.fragment);
attr(p, "class", "svelte-urs9w7");
},
m(target, anchor) {
insert(target, p, anchor);
insert(target, t1, anchor);
mount_component(nested, target, anchor);
current = true;
},
p: noop,
i(local) {
if (current) return;
transition_in(nested.$$.fragment, local);
current = true;
},
o(local) {
transition_out(nested.$$.fragment, local);
current = false;
},
d(detaching) {
if (detaching) detach(p);
if (detaching) detach(t1);
destroy_component(nested, detaching);
}
};
}

class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, null, create_fragment, safe_not_equal, {});
}
}

export default App;

可以看到在对应的fragment声明周期中加入了create_component、mount_component等component的生命周期
通过查阅源码src/runtime/internal/Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
export function create_component(block) {
block && block.c();
}

export function claim_component(block, parent_nodes) {
block && block.l(parent_nodes);
}

export function mount_component(component, target, anchor, customElement) {
const { fragment, on_mount, on_destroy, after_update } = component.$$;

fragment && fragment.m(target, anchor);

if (!customElement) {
// 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);
}

export 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 = [];
}
}

其实就是调用了fragment的生命周期,所以svelte 的 runtime 主要由 fragment 和 component 组成,而 component 是包含了 fragment。它们有着独立的生命周期,将逻辑层和渲染层分离。

Svelte的数据流是什么样的呢?
我们来看一个例子

1
2
3
4
5
6
7
8
9
10
11
<script>
let count = 0;

function handleClick() {
count += 1;
}
</script>

<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

会被编译为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/* App.svelte generated by Svelte v3.42.4 */
import {
SvelteComponent,
append,
detach,
element,
init,
insert,
listen,
noop,
safe_not_equal,
set_data,
space,
text
} from "svelte/internal";

function create_fragment(ctx) {
let button;
let t0;
let t1;
let t2;
let t3_value = (/*count*/ ctx[0] === 1 ? 'time' : 'times') + "";
let t3;
let mounted;
let dispose;

return {
c() {
button = element("button");
t0 = text("Clicked ");
t1 = text(/*count*/ ctx[0]);
t2 = space();
t3 = text(t3_value);
},
m(target, anchor) {
insert(target, button, anchor);
append(button, t0);
append(button, t1);
append(button, t2);
append(button, t3);

if (!mounted) {
dispose = listen(button, "click", /*handleClick*/ ctx[1]);
mounted = true;
}
},
p(ctx, [dirty]) {
if (dirty & /*count*/ 1) set_data(t1, /*count*/ ctx[0]);
if (dirty & /*count*/ 1 && t3_value !== (t3_value = (/*count*/ ctx[0] === 1 ? 'time' : 'times') + "")) set_data(t3, t3_value);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(button);
mounted = false;
dispose();
}
};
}

function instance($$self, $$props, $$invalidate) {
let count = 0;

function handleClick() {
$$invalidate(0, count += 1);
}

return [count, handleClick];
}

class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}

export default App;

可以看到,对比不需要进行数据变更的第一个例子,多出了 instance、p两个函数,
p 函数是编译生成的最终的产物,是原生的js可以直接运行在浏览器里,会在有脏数据时被调用。p 函数唯一做的事情就是,当 count 发生变更的时候,调用原生方法把 t3 这个原生DOM节点更新。
而在instance函数中对可变的变量用$$invalidate 包裹起来
svelte是单向数据流的

查阅源码src/runtime/internal/Component

1
2
3
4
5
6
7
8
9
10
$$.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;
})
: [];

instance 方法会返回一个数组,里面包括组件实例的一些属性和方法。Svelte 会把返回 instance 方法的返回值赋到 ctx 上保存。所以这里的 i 就是 instance 返回的数组下标。
在编译阶段,Svelte 会按照属性在数组中的位置,生成对应的数字。
接着往下,$$invalidate中判断赋值之后不相等时就会调用 make_dirty。

1
2
3
4
5
6
7
8
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 的组件添加到 dirty_components 中,然后调用了 schedule_update 方法。

1
2
3
4
5
6
export function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}

schedule_update 就是,在 Promise.resolve(microTask) 中调用 flush 方法。

1
2
3
4
5
6
7
export function flush() {
for (let i = 0; i < dirty_components.length; i += 1) {
const component = dirty_components[i];
set_current_component(component);
update(component.$$);
}
}

flush 方法,循环调用dirty_components中每个需要更新组件的 update 方法。

1
2
3
4
5
6
7
8
function update($$) {
if ($$.fragment !== null) {
$$.update();
const dirty = $$.dirty;
$$.dirty = [-1];
$$.fragment && $$.fragment.p($$.ctx, dirty);
}
}

调用了fragment的p方法

总结一下

  • 修改数据,调用 $$invalidate 方法
  • 判断是否相等,标记脏数据,make_dirty
  • 在 microTask 中触发更新,遍历所有 dirty_component, 更新 DOM 节点
  • 重置 Dirty

Svelte会消失么

Svelte 库主要由 node_modules/svelte 目录中的.js 文件定义。主要函数在 internal.js 中定义,目前大约有 1400 行代码。

其他库文件则针对特定功能,具体包括:

  • easing.js
  • motion.js
  • register.js
  • store.js
  • transition.js

输入 npm run build 会在 public 目录中生成文件,包括 bundle.js。应用程序使用的 Svelte 库函数将复制到 bundle.js 的顶部。后文展示的 Todo 应用程序中,这里大约是 500 行代码。

因此 Svelte 库代码不会消失,只是它与其他 Web 框架相比体积很小。

存在的一些问题

  1. 作为组件和外部的通信只能通过window
  2. 无法实现跨平台

参考文档
硬核基础二进制篇(一)0.1 + 0.2 != 0.3 和 IEEE-754 标准

硬核基础二进制篇(二)位运算