Svelte是什么?
Svelte is a component framework — like React or Vue — but with an important difference.
Svelte的作者Rich Harris(Rollup,Buble的作者)在Svelte 3: Rethinking reactivity这篇文章中如此的介绍,Svelte是一个类似React或者Vue的组件框架。
但是随后也介绍它与React和Vue不同的特性,Rich Harris在Virtual DOM is pure overhead这篇文章中认为:
1. 虚拟DOM本身是一种性能的开销。
2. 通过虚拟DOM对比更新DOM也只是一种 ‘通常情况下足够快’的方案。
3. 通过虚拟DOM的更新需要3步,生成新的虚拟DOM -> 对比新旧虚拟DOM -> 真实DOM的更新,而大部分的更新情况(例如相同的DOM节点只更新了某个属性)只有第三步有价值,因为应用程序的基本结构没有改变。
4. 虚拟DOM是一种达到声明式、状态驱动UI开发的手段,因为它允许开发者在构建应用程序时无需考虑状态变化,而且性能通常足够好。
Svelte未采用虚拟DOM,而是通过建立数据与真实DOM之间的映射关系,实现数据变化直接更新相关DOM的能力。
语法和基本概念
Svelte采用了模板语法,所以在一些语法特性上与Vue有类似和相通的一些地方。下面通过一些基础的语法和特性来了解一下这个新框架。
使用环境
1. 在线环境。由官方提供的在线编码环境,实时编译结果,还支持查看编译后的代码,有助于增强对Svelte的理解。本文中的示例也都是通过在线环境进行编写。
2. degit。Svelte 3并未提供官方的脚手架,不过可以通过degit拉取一个Svelte项目的模板。
npx degit sveltejs/template my-svelte-project
拉取成功后通过npm i安装依赖,然后启动项目即可。
组件
组件是一个可重用的、自包含的代码块,将 HTML、CSS 和 JavaScript 代码封装在一起并写入 .svelte 后缀名的文件中即可构成一个组件。
例如在一个App.svelte组件写入如下代码即可构成一个最简单的组件。相比Vue而言,Svelte无需template标签包裹。
<h1>Hello world!</h1>
插值
Svelte采用了单括号 {} 的插值语法,跟React相同。在script代码块中直接声明变量即可在元素标签内进行使用。
<script>
let name = 'world';
</script>
<h1>Hello {name}!</h1>
CSS样式
Svelte组件的样式是互相隔离的。在style代码块中直接编写样式即可。
<style>
p {
color: purple;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
<p>This is a paragraph.</p>
通过查看编译后的CSS,发现样式会变成带上.svelte-urs9w7这种特殊哈希字符的class。
p.svelte-urs9w7{color:purple;font-family:'Comic Sans MS', cursive;font-size:2em}
而在编译的JS中,发现用来创建页面元素的函数中多了添加css属性的代码:
c() {
p = element("p");
p.textContent = "This is a paragraph.";
// 给P标签添加组件特有标识的class
attr(p, "class", "svelte-urs9w7");
},
列表渲染
Svelte提供了{#each}…{/each}语句块用来渲染数据列表。在列表渲染这点上,各个框架都各有特色,Vue是通过v-for指令,React则是通过使用React元素数组。
<script>
let cats = [
{ id: 'J---aiyznGQ', name: 'Keyboard Cat' },
{ id: 'z_AbfPXTKms', name: 'Maru' },
{ id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
];
</script>
<ul>
{#each cats as cat, i}
<li>
{i + 1}: {cat.name}
</li>
{/each}
</ul>
条件渲染
Svelte提供了{#if}…{/if}语句块用来在模板中做逻辑处理。与条件渲染类似,Vue则是使用v-if指令,而React提供的JSX是JS的扩展,可以直接使用原生语法的逻辑判断语句。
<script>
let user = { loggedIn: false };
function toggle() {
user.loggedIn = !user.loggedIn;
}
</script>
{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{:else}
<button on:click={toggle}>
Log in
</button>
{/if}
除了{#if},Svelte也同时提供了{:else},{:else if}来完善逻辑语法。
事件
Svelte对于事件的监听十分简单,直接使用on:指令即可。
<script>
let count = 0;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
响应式
Svelte与React,Vue一样提供了响应式编程的能力,让开发者可以聚焦在数据变化上,而无需操心视图的变化。
在事件的demo中,count就是一个响应式数据,当count每一次加1, 按钮的文本就会自动更新。
Svelte使用赋值语句提供响应式能力。相比Vue需要提前在data函数返回值中声明再使用和React需要调用setState来驱动视图更新, Svelte简洁了很多。仅仅只需要正常声明再赋值即可。
Svelte还有更多的语法和API可以自行到官方文档-中文进行查阅和体验,这里就不过多讲解了。
响应式原理的浅析
Svelte既然不采用虚拟DOM,又是提供响应式能力的呢?
Svelte源码分成两部分,compiler 和 runtime。compiler主要负责将Svelte的语法转化成JS代码。
在这里借用事件一节中demo做示例。在在线环境下,拿到转换后的代码。
/* App.svelte generated by Svelte v3.43.0 */
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 {
// created函数 用于创建元素
c() {
button = element("button");
t0 = text("Clicked ");
t1 = text(/*count*/ ctx[0]);
t2 = space();
t3 = text(t3_value);
},
// mounted函数 用于处理创建的元素在页面中的行为
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;
}
},
// update函数 用于更新元素
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,
// destory函数 用来处理元素销毁时的行为
d(detaching) {
if (detaching) detach(button);
mounted = false;
dispose();
}
};
}
// 用于管理script代码块中声明的变量和函数,提供了组件的上下文
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;
转换后的结构
转换后的代码主要分为三部分。主要是 create_fragment 函数,instance 函数,App 类。
create_fragment函数返回了一个对象,里面主要包含了如何创建页面元素(c函数)、如何构建和组织页面元素(m函数)以及如何更新页面元素(p函数)的方法。
instance 函数主要是转换了 script 代码块里的 变量以及行为。
App 类主要就是继承了SvelteComponent同时调用了init方法。
整体流程
Svelte的整体流程并不复杂。组件会被转成原生的Class。入口文件会调用这个类。例如最简单的挂载到页面上仅仅只需要一个容器(DOM元素)。
const app = new App({
target: document.body,
});
随后会init方法会调用create_fragment函数,拿到组件的各个生命周期函数。调用c函数创建页面元素,根据配置执行一些内置函数,主要是生成一些属性和执行生命周期钩子。以及在Svelte调度器中推入一些内置的callback,最后通过调用m函数来完成页面元素的挂载。
响应式原理
在官方文档中提到,Svelte 其实是将赋值语句替换为一些代码,这些代码将通知 Svelte 更新 DOM。
通过查看转换后的JS代码。也确实发现赋值语句会被替换成一个名为$$invalidate的函数。
再通过翻查源码 runtime 中的函数发现。$$invalidate是一个在init阶段传递的匿名函数。
// instance就是instance函数,$$invalidate就是匿名的箭头函数。
// ctx就是instance的返回值。可以认为ctx为组件的上下文 里面包含了变量以及函数的行为。
// i 对应了 此次赋值变量的位置,例如count在ctx中位置为0。handleClick中$$invalidate的传参第一个参数就是0。
$$.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;
})
: [];
当值发生变化时并且组件处于ready阶段(前置工作已完成,例如更新current_component指针,$$属性初始化等等)。$$invalidate被调用。执行 make_dirty 方法。源码如下:
// 用于标记脏组件(通知哪个组件更新),脏数据(标记需要更新的数据),以及在微任务队列中推入flush方法。
function make_dirty(component, i) {
if (component.$$.dirty[0] === -1) {
dirty_components.push(component);
// 延迟调用flush方法
schedule_update();
component.$$.dirty.fill(0);
}
// 更新标记 dirty根据i生成二进制标记
// 通过dirty中的标记可以知道需要执行p函数中哪些更新逻辑
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
}
在 make_dirty 方法中, Svelte通过二进制标志位对脏数据进行记录。
例如count - ctx (instance函数返回值) 中索引第一个变量 进行了修改:
1. component被推入到dirty_components中,
2. schedule_update()在微任务中推入flush(用来执行调度器内所有任务队列的函数)
3. component.$$.dirty[0]的标记从-1被重置到0 ,然后通过索引计算结果得到标记为1。
4. 当flush执行时,会调用组件的更新函数(p函数),而p函数根据dirty标记与在编译阶段就得到的映射(常量数值)进行 按位与 & 就可以知道哪一部分可以执行set_data。
Svelte的响应式原理意味着无法侦测到数组通过push,pop等方式的修改,必须通过赋值方式修改数组和对象。
Svelte,React,Vue对于常用语法或特性的支持程度与一些差异
Svelte | React | Vue | |
---|---|---|---|
描述ui | 模板 | JSX | 模板或JSX |
组件 | 支持 | 支持 | 支持(需额外在component中注入) |
动态属性 | attr= | attr= | attr={{attr}} |
样式隔离 | 支持 | 支持 | 支持 |
响应式能力 | 支持 | 支持 | 支持 |
Props | 支持 | 支持 | 支持 |
条件渲染 | 原生JS的条件判断 | v-if | |
列表渲染 | React元素数组 | v-for | |
事件绑定 | on: | on+[事件名]属性 | v-on |
动态class | 支持 | 支持 | 支持 |
绑定数据 | bind: | 不支持 | v-bind |
Context | 支持 | 支持 | 提供类似作用的provide/inject |
Slot | 支持 | 不支持,但可通过Props直接传递组件或元素 | 支持 |
生命周期函数 | 支持 | 支持 | 支持 |
可以看到,三个框架基本都支持在上述列到的常用语法或者提供了类似的语法。
性能
在A RealWorld Comparison of Front-End Frameworks 2020的测评报告中,有三个衡量指标:
1.Performance 性能
通过chrome的性能面板比较,Svlete的得分已经拿到了并列第一的名次。而React,Vue应用的性能得分已经处于中游了。
2.Size 大小
而在包大小的方面,通过Gzip压缩后的包仅仅只有15kb,直接拿到第一的位置。不过这也是因为Svelte消除了运行时,打包产物存在大量重复的构建节点等代码,这也让打包后的代码在Gzip中极其受益。
3.Lines of Code 代码行数
而完成相同的应用,使用Svlete的代码量也是远远的少于其他框架。
同时,翻看了2019年的报告,对比过后,Svelte在包大小和代码行数上依旧保持着领先,这得益于它的核心思想(通过静态编译减少运行时),但是在性能上经过一年有了翻天覆地的变化,直接从中游的位置来到了第一的位置。
不仅如此,另外一个前端框架性能对比的项目也给出了同样的答案。这足以证明Svelte在性能、体积上具有强大的优势。
生态环境
Svelte | React | Vue | |
---|---|---|---|
TS | 支持 | 支持 | 支持 |
状态管理 | 自带 | Redux/Mobx等等 | Vuex |
UI | Material UI | 太多了 | 太多了 |
路由 | Svelte-router | React-router | Vue-router |
服务端渲染 | 支持 | 支持 | 支持 |
测试工具 | 官网无相关内容 | Jest | Vue Test Utils |
脚手架 | 无 | create-react-app | vue-cli |
在生态环境这方面,Svelte呈现了明显的劣势,在国内没有成熟的生产环境的ui库,不过这也是因为Svelte诞生较晚,而早期的版本优势也并不明显。 |
总结
在React,Vue两个成熟且生态环境十分完善的框架盛行的趋势下,Svelte作为一个新兴的前端框架,以一个完全不同的设计思路实现了响应式的编程范式,不管是性能,还是语法的完善度和易上手程度都是十分可观的,但是构建项目不仅仅只是考虑框架的可行性,个人看来,Svelte的生态环境还不足以直接支撑大型项目的落地,在国内缺少成熟的UI库,脚手架工具的缺少也意味着Svelte在集成这一块的薄弱性,同时Svelte的原理意味着修改数据的方式并没有那么贴近原生语法,依赖赋值语句。
总的来说,Svelte相对于其他框架的优势在项目生产力上并不明显。在编程思想上也仅仅只是响应式的另一种具现。不过基于它的特性和性能以及现有的生态环境,感兴趣的朋友还是值得一试。