简介
Svelte 是一个构建 web 应用程序的工具,与 React 和 Vue 等 JavaScript 框架类似,都怀揣着一颗让构建交互式用户界面变得更容易的心。
但是有一个关键的区别:Svelte 在 构建/编译阶段 将你的应用程序转换为理想的 JavaScript 应用,而不是在 运行阶段 解释应用程序的代码。这意味着你不需要为框架所消耗的性能付出成本,并且在应用程序首次加载时没有额外损失。
你可以使用 Svelte 构建整个应用程序,也可以逐步将其融合到现有的代码中。你还可以将组件作为独立的包(package)交付到任何地方,并且不会有传统框架所带来的额外开销。
特点
代码简洁
我们以一个简单例子来说明,在输入框中输入内容,然后在弹窗中显示相关内容。然后将svelte的代码与react、vue作一下对比,可以很明显的发现,svelte要写的代码量远少于react和vue。
使用svelte,代码如下:
<script>
let animal = 'dog';
const showModal = () => {
alert(`My favorite animal is ${animal}`);
};
</script>
<input type="text" bind:value={animal} />
<button on:click={showModal}>弹出</button>
react:
import React, { useState } from 'react';
export default function App() {
const [animal, setAnimal] = useState('dog');
const showModal = () => {
alert(`My favorite animal is ${animal}`);
};
return (
<>
<input
type="text"
value={animal}
onChange={() => {
setAnimal(animal);
}}
/>
<button onClick={showModal}>弹出</button>
</>
);
}
Vue
<template>
<div>
<input type="text" v-model="animal" />
<button @click="showModal">弹出</button>
</div>
</template>
<script>
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const animal = ref('dog');
const showModal = () => {
alert(`My favorite animal is ${animal.value}`);
};
return {
animal,
showModal,
};
},
});
</script>
无虚拟dom
Svelte 能够将代码编译成体积小、不依赖框架的普通js代码,让应用程序无论是启动还是运行都很迅速。
性能更好
许多同学在学习 react 或者 vue 时可能听说过诸如“虚拟dom很快”之类的言论,所以看到这里就会疑惑,svelte 没有虚拟dom,为什么反而更快呢?
这其实是一个误区,react 和 vue 等框架实现虚拟 dom 的最主要的目的不是性能,而是为了掩盖底层 dom 操作,让用户通过声明式的、基于状态驱动UI的方式去构建我们的应用程序,提高代码的可维护性。
另外 react 或者 vue 所说的虚拟 dom 的性能好,是指我们在没有对页面做特殊优化的情况下,框架依然能够提供不错的性能保障。例如以下场景,我们每次从服务端接收数据后就重新渲染列表,如果我们通过普通dom操作不做特殊优化,每次都重新渲染所有列表项,性能消耗比较高。而像react等框架会通过key对列表项做标记,只对发生变化的列表项重新渲染,如此一来性能便提高了。
![10bb11e8cdbe2d4a5e54c2f2fbb40a7d.png](https://i-blog.csdnimg.cn/blog_migrate/1dbc8607b3539ed2cf148325af0e77f1.png)
思考上面这个场景,如果我们操作真实dom时也对列表项做标记,只对发生变化的列表项重新渲染,省去了虚拟dom diff等环节,那么性能是比虚拟dom还要高的。
svelte便实现了这种优化,通过将数据和真实dom的映射关系,在编译的时候通过 ast 计算并保存起来,数据发生变动时直接更新dom,由于不依赖虚拟dom,初始化和更新时都都十分迅速。
体积更小?
我们都知道 react 和 vue 都是基于运行时的框架,打包后除了用户自己编写的代码之外,还有框架本身的 runtime。而 svelte 是通过静态编译减少框架运行时的代码量。
https://www.npmtrends.com/react-vs-react-dom-vs-vue-vs-svelte[1]
参照 npm trends,react、vue和svelte的 minzipped 体积分别为:42.2kb、22.9kb和1.6kb,足以看出 svelte 的短小精悍。
但是上面这个单看框架的体积稍微有些片面,svelte 由于在编译时将组件直接解释为 js,所以相对来说组件编译后的代码量会比 vue 和 react 编译后要大一些。假如有 n 个组件,svelte 每个组件编译后个规模为 a,vue 或者 react 每个组件编译后的规模为 b:
svelte:体积 = a * n
vue:体积 = S + b * n (S为框架运行时的体积)
在 a > b 的情况下,随着 n 的数量的增多,svelte 项目在体积上并不会占据太大的优势。
与 vue 对比
Vue 方面,尤雨溪曾将 vue3 和 svelte 做了对比:https://github.com/yyx990803/vue-svelte-size-analysis[2]
基于真实的 todomvc 场景构建组件,编译以后Svelte 的组件输出大小是Vue的1.7倍,在 SSR 的情况下,这一比例会上升到2.1倍。在不开启 SSR 的情况下,大概19个组件后就会抹平运行时体积的大小差异,开启 SSR 的情况下,大概 13 个组件后就会抹平差异。
与 react 对比
Jacek Schae 也曾将 svelte 和 react进行对比,也是在组件数量达到一定的阈值之后, svelte 的体积优势就不再存在。
可见,大型项目中使用 svelte 的体积问题还有待考究。
真正的reactivity
无需复杂的状态管理库,Svelte 为 JavaScript 自身添加反应能力。后面的源码解读部分会讲解 svelte 的响应式实现。
发展趋势
Svelte 是 Rich Harris[3] (rollup 作者),2016 年 svelte 开始开源, 2019 年开始引起较为广泛的关注。
Github 上 svelte[4] 现在是 49.9k star:
Npm 上 svelte[5] 的周下载量大概在 15w 左右:
虽然从 star 数和下载量来说离 react、vue 和 angular 还有较大差距,但是鉴于其出道比较晚也是可以理解。而且从框架的调研[6]来看,近两年来其用户满意度和感兴趣度都是高居第一,使用和知名度也是在急速上升的。
总体来看,未来可期!
源码解读
svelte 的源码由两大部分组成,compiler 和 runtime。compiler 的作用是将 svelte 模版语法编译为浏览器能够识别的js SvelteComponent,而 runtime 则是在浏览器中帮助业务代码运作的运行时函数。
complier
Svelte 如其介绍所说,在 complier 阶段完成了大部分的工作,而 complier 又分为 parse 和 complie 两部分:
parse
parse 会读取 .svelte
文件的内容进行解析。
对于 html 部分的内容,会分为 tag、mustache、text 三种解析类型:
tag: tag 解析的内容以
<
作为标识,包括 HTMLElement、style标签、script标签以及用户自定义的 svelte 组件以及 svelte 实现的一些特殊标签如svelte:head
、svelte:options
、svelte:window
以及svelte:body
等。muscache:mustache 以
{
作为标识,识别的内容除了模板语法之外,还包括 svelte 的逻辑渲染(else……if、each)等语法、{``@html``}
、{``@debug}
等。text就是无语义的静态文本
最终parse会将.svelte
的内容解析成含有 html
、css
、instance
、module
四部分的ast。
Instance 是指 script 标签中响应式的属性和方法,module 是使用 <script context="module"
声明 的无响应的变量和方法。
complie
Complie 首先会将 parse 过程中拿到的语法树(包含 html,css,instance 和 module)转换为 Component,然后在 render_dom 中通过 code-red 中的 print 函数将component 的转换为 js 可运行代码,最终输出 complier 的结果。
runtime
我们以一个简单的例子来看下,点击按钮,count加1:
<script>
let count = 0;
const addCount = () => {
count += 1;
};
</script>
<div>
<button on:click={addCount}>增加</button>
<p>count is: {count}</p>
</div>
svelte编译后的结果为:
/* 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 div;
let button;
let t1;
let p;
let t2;
let t3;
let mounted;
let dispose;
return {
c() {
div = element('div');
button = element('button');
button.textContent = '增加';
t1 = space();
p = element('p');
t2 = text('count is: ');
t3 = text(/*count*/ ctx[0]);
},
m(target, anchor) {
insert(target, div, anchor);
append(div, button);
append(div, t1);
append(div, p);
append(p, t2);
append(p, t3);
if (!mounted) {
dispose = listen(button, 'click', /*addCount*/ ctx[1]);
mounted = true;
}
},
p(ctx, [dirty]) {
if (dirty & /*count*/ 1) set_data(t3, /*count*/ ctx[0]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(div);
mounted = false;
dispose();
},
};
}
function instance($$self, $$props, $$invalidate) {
let count = 0;
const addCount = () => {
$$invalidate(0, (count += 1));
};
return [count, addCount];
}
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
export default App;
我们看到编译后的结果中,有一个 create_fragement
的方法和 instance
方法。
另外还从 svelte/internal
引入了 append
、detach
、element
、insert
、listen
等方法,从源码[7]中可以知道都是一些很简单的对原生 dom 操作的封装。
create_fragment
create_fragment
是和每个组件生成 dom 相关的方法,里面定义了 c
、 m
、p
、i
、o
、d
等一系列内置方法,从缩写上不好理解,我们可以看下源码[8]中其类型定义:
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(create):create 函数里会对一系列的 dom 节点进行创建,并将它们保存在 create_fragment 的闭包中。
m(mount):mount 函数中会对根据 dom 节点的层级关系,构建 dom 树,并将 dom 树插入到页面 target 节点下。同时还会将 dom 上相关的事件监听进行绑定,然后将组件的 mounted 状态置为 true。
p(update):组件的状态发生改变时会触发 update 函数,对 dom 中相应的数据重新进行更新渲染。
d(distroy):将 dom 挂载从页面中移除,将组件的 mounted 状态置为 false,同时移除一系列的事件监听。
instance
instance
方法中返回了包含组件实例中属性和方法的数组,将相应的数据绑定在组件实例的 $$.ctx
上,并且根据用户定义的触发属性修改的方法去调用一个 $$invalidate
方法,我们来看下$$invalidate
这个方法干了什么:
$$.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 "i] "i]) $.bound[i") $.bound[i");
if (ready) make_dirty(component, i);
}
return ret;
});
$$invalidate
接收2个或更多参数。第一个参数是 i 是 属性在 $$.ctx
,第二个参数 ret 是定义的属性改变的逻辑函数。然后判断属性重新赋值后与之前的值是否相等,若不相等,则会调用 make_dirty
更新相关的 ui。
脏检测
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));
}
每个组件的 $$
属性上有一个 dirty 数组,用于标记 instance 中需要更新的属性下标,当 dirty 第一项为 -1
时,表示这个组件当前是干净的,将其 push 到 dirty_components 中,然后执行 schedule_update
方法。
schedule_update:
schedule_update 中会异步去执行 flush 函数:
export function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
flush:
flush 中对刚刚的 dirty_components 进行遍历,执行 update 函数.
for (let i = 0; i < dirty_components.length; i += 1) {
const component = dirty_components[i];
set_current_component(component);
update(component.$$);
}
Update
update函数会调用组件 update
生命周期钩子函数,将 dirty 数组重新置为 -1,然后调用 fragment 的 p(update) 去更新ui。
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);
}
}
dirty 标记
回到上面的 make_dirty
方法,svelte 是通过如下操作对属性进行脏标记的:
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
了解了位运算,那我们看上面脏标记的代码,(i / 31) | 0
对每个 instance 返回的数组下标除以 31 后和 0 做或运算,即除 31 向下取整,(1 << (i % 31))
i 对 31 取余之后向左进行移位操作。通过上述的两步操作,可以了解到 dirty 数组存储了一系列的 32 位整数,通过这一操作,提高了内存利用率,每个数组项可以存储31个属性是否需要更新。
例如如下32位的整数43,对应的32位二进制为:
Dirty = [43]
43 -> 0000 0000 0000 0000 0000 0000 0010 1011
二进制中为1的位代表需要更新的 instance 中数组第几项,即第1、2、4、6项属性需要更新。
整体流程
![9e29a2f57e7ddb6afb512166f2fadd95.png](https://i-blog.csdnimg.cn/blog_migrate/3f2e064b515b1b956adafcb2e9bfbe9d.png)
周边生态
状态管理
Svelte 框架中自己实现了 store[9],无需安装单独的状态管理库。
路由
Svelte 官方目前没有自己的路由,社区实现的路由库:
svelte-routing[10],1.4k star,npm 周下载量 3.5k,和 react-router-dom 的使用方式很像
svelte-spa-router[11],886 star,npm 周下载量 3.5k,更类似于 vue-router 的使用方式
SSR
sveltekit[12]
目前官方主推的 ssr 框架,具备以下的特点:
服务端渲染(SSR)
路由
typescript支持
less, scss支持
serverless
vite打包
Sapper[13]
sapper开发比较早,也是官方的 ssr 框架,但是 Rich Harris 在2020年10月的svelte峰会上表示:sapper永远不会发布1.0版本。也就是说 sapper 不会发布稳定版甚至被放弃,而 svelte kit 则是它的继任者。
跨平台
Svelte 偏向于性能,目前在跨平台方面还没有进行探究。
native
svelte-native[14] (社区库)
小程序
暂不支持
桌面应用
可以 electron[15] 结合开发桌面应用
组件库
Svelte 现在组件库数量尚可,但是都不够完备,如 table 等复杂组件都没有实现
svelte-material-ui[16] 1.8k star,扁平化设计,偏向 google 的 UI 设计系统。
carbon-components-svelte[17] 1k star,偏商务,非主流,偏向 IBM 的开源设计系统。
smelte[18] 1k star,material 的另一种方案
测试工具
缺少官方的测试工具,社区单元测试库:
svelte-testing-library[19]
总结来说,svelte 的周边生态目前还不够完备,但由于起步较晚可以理解。
VSCode插件
Svelte for VS Code[20] 识别 svelte 语法,对 svelte 进行语法高亮
Svelte 3 Snippets[21] 在 vscode 中通过简写快速生成 svelte 相关语法
Svelte Intellisense[22] svelte 组件 command + click 快速跳转及 svelte 引入省略
.svelte
文件后缀
Typescript
支持
CSS 预处理器
支持 less、 scss 及 postcss
使用 svelte 构建 web component
我们平台组最近正在进行 web component 组件库开发的选型调研,svelte 也作为备选的框架之一。传统的框架如 vue、react 如果想要开发web component,需要每个组件都打包一份体积庞大的运行时,而 svelte 的运行时会根据你的功能按需引入,所以十分适合 web component 的开发场景。
配置
下面是通过 svelte 开发一个简单的 web component 的实例:
通过官方提供的脚手架创建一个组件
npx degit sveltejs/component-template custom-test-button
修改相关的文件配置:
修改 package.json
包名称
{
"name": "CustomTestButton",
"svelte": "src/index.js",
"module": "dist/index.mjs",
"main": "dist/index.js",
"scripts": {
"build": "rollup -c",
"prepublishOnly": "npm run build"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^9.0.0",
"rollup": "^2.0.0",
"rollup-plugin-svelte": "^6.0.0",
"svelte": "^3.0.0"
},
"keywords": [
"svelte"
],
"files": [
"src",
"dist"
]
}
修改 rollup.config.js
文件的内容:
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import pkg from './package.json';
const name = pkg.name
.replace(/^(@\S+/)?(svelte-)?(\S+)/, '$3')
.replace(/^\w/, (m) => m.toUpperCase())
.replace(/-\w/g, (m) => m[1].toUpperCase());
export default {
input: 'src/index.js',
output: [
{ file: pkg.module, format: 'es' },
{ file: pkg.main, format: 'umd', name },
],
plugins: [svelte({ customElement: true }), resolve()],
};
增加组件内容
如下定义了一个组件内容
<svelte:options tag="custom-test-button" />
<script>
export let value = '点击';
export let type = 'default';
</script>
<button class={`custom-test-button ${type}`}>{value}</button>
<style>
.custom-test-button {
height: 32px;
padding: 0 8px;
box-sizing: border-box;
line-height: 32px;
font-size: 14px;
border: 1px solid rgba(0, 0, 0, 0.2);
background-color: #fff;
}
.primary {
background-color: #42b983;
color: #fff;
border: none;
}
.danger {
background-color: #f44336;
color: #fff;
border: none;
}
</style>
在项目目录执行
npm run build
将组件打包,假设打包后的文件为index.js
使用
Html 引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>svelte web component</title>
</head>
<body>
<script src="./index.js"></script>
<custom-test-button value="测试按钮" type="danger"></custom-test-button>
</body>
</html>
vue 引入
在 vue 的 html 中引入 index.js
:
<body>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
properly without JavaScript enabled. Please enable it to
continue.</strong
>
</noscript>
<div id="app"></div>
<script src="./custom.js"></script>
<!-- built files will be auto injected -->
</body>
然后在 vue 组件中使用
<template>
<div>
<custom-test-button :value="'测试'" type="danger"></custom-test-button>
</div>
</template>
React 中引入
同 vue,就不做过多介绍了
总结
整体来说,svelte 继前端三大框架之后推陈出新,以一种新的思路实现了响应式,由于起步时间不算很长,目前来说其生态还不够完备, 在大型项目中的应用目前也还有待考究,但是在一些简单页面如活动页、静态页等场景感觉目前还是十分适合的,个人对其未来发展表示看好。
由于其简洁的语法以及与 vue 语法相似的特点,上手成本十分小,大家感兴趣可以稍花一点点时间了解一下,丰富自己的武器库。
参考资料
[1]
https://www.npmtrends.com/react-vs-react-dom-vs-vue-vs-svelte: https://www.npmtrends.com/react-vs-react-dom-vs-vue-vs-svelte
[2]https://github.com/yyx990803/vue-svelte-size-analysis: https://github.com/yyx990803/vue-svelte-size-analysis
[3]Rich Harris: https://github.com/Rich-Harris
[4]svelte: https://github.com/sveltejs/svelte
[5]svelte: https://www.npmjs.com/package/svelte
[6]调研: https://2020.stateofjs.com/en-US/technologies/front-end-frameworks/
[7]源码: https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/dom.ts
[8]源码: https://github.com/sveltejs/svelte/blob/master/src/runtime/internal/Component.ts
[9]store: https://github.com/sveltejs/svelte/blob/master/src/runtime/store/index.ts
[10]svelte-routing: https://github.com/EmilTholin/svelte-routing
[11]svelte-spa-router: https://github.com/italypaleale/svelte-spa-router
[12]sveltekit: https://github.com/sveltejs/kit
[13]Sapper: https://github.com/sveltejs/sapper
[14]svelte-native: https://github.com/halfnelson/svelte-native
[15]electron: https://github.com/electron/electron
[16]svelte-material-ui: https://github.com/hperrin/svelte-material-ui
[17]carbon-components-svelte: https://github.com/carbon-design-system/carbon-components-svelte
[18]smelte: https://smeltejs.com/
[19]svelte-testing-library: https://github.com/testing-library/svelte-testing-library
[20]Svelte for VS Code: https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode
[21]Svelte 3 Snippets: https://marketplace.visualstudio.com/items?itemName=fivethree.vscode-svelte-snippets
[22]Svelte Intellisense: https://marketplace.visualstudio.com/items?itemName=ardenivanov.svelte-intellisense