Svelte 本意是苗条的,细长的,优美的;
2021年,StackOverflow Developer 调查中,Svelte 成为最受开发者欢迎的 Web 框架。
官方的介绍:Svelte 是一种全新的构建用户界面的方法。传统框架如 React 和 Vue 在浏览器 中需要做大量的工作,而 Svelte 将这些工作放到构建应用程序的编译阶段来处理。与使用虚拟(virtual)DOM 差异对比不同。Svelte 编写的代码在应用程序的状态更改时就能像做外科手术一样更新 DOM。
可以看见Svelte是和React、Vue一样的UI框架,它最关键的不同在于 Svelte在编辑阶段将应用程序转换为高效的JavaScript代码,减少了运行时框架代码,因此我们可以节省出框架所带来的性能成本,同时保留了 组件化、响应式 等优势;
其核心还是在于通过静态编译将代码编译为高效的原生操作,减少框架运行时的代码量
,这同时也意味着Svelte编译过后的单组件,是可以在其他任意框架中使用的,并不冲突;
Svelte的特点提取总结如下:
编译阶段
:在构建阶段,Svelte 将组件编译成高效的 JavaScript 代码,而不是使用运行时框架。这可以减少包的大小,提高应用程序性能- 更少的引导代码:Svelte 应用程序通常只需在浏览器中包含一个小的运行时库。这与 React 和 Vue 不同,它们需要包含更多的 runtime 依赖
- 易于上手:Svelte 的语法非常简洁,对新手来说易于上手。组件主要由 HTML、CSS 和 JavaScript 组成,并且语法非常直观
无虚拟 DOM
:React 和 Vue 的性能优化依赖于虚拟 DOM。虽然虚拟 DOM 的使用可以显著提高大型应用程序的性能,但在更新 DOM 时存在一定的开销。Svelte 不使用虚拟 DOM,而是在编译阶段生成最小的 DOM 更新代码- 内置转换:与其他框架不同,Svelte 内置了对转场的支持。这使得在组件之间创建动画和过渡效果变得非常简单。开发者不需要引入额外的依赖项就可以创建令人印象深刻的动画效果
- 可扩展性:Svelte 提供了出色的可扩展性,您可以随着应用程序的需求增长而增加功能。您还可以选择将 Svelte 与其他框架和库(例如:Redux、MobX 和 GraphQL)一起使用 例如 Redux,可以使用 svelte-redux-connect 库链接 store与UI;同时 Svelte也内置了状态管理能力;
- 非侵入性:Svelte 是一个非侵入性的框架。你可以在现有的项目中逐步引入 Svelte ,而不是立即进行大规模迁移。因此,它也可以与其他框架共存 Svelte本身不会包含太多的运行时库,只需要在 webpack 或者 rollup 中添加相应的 .svelte 文件解析loader,最后挂载到特定的节点上即可;
- 社区支持:虽然 Svelte 相对较新,但随着时间的推移,该框架的社区不断增长,越来越多的第三方库和资源供开发者使用
1. 从编译结果看
svelte的文件结构,以 App.svelte 文件编译为例
js
<script>
let value;
function handleClick() {
value = value + 1;
}
</script>
<style></style>
<main on:click={handleClick}>Hello {value}!</main>
1.1 APP
js
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, instance, create_fragment, safe_not_equal, { name: 4 });
}
}
1.2 create_fragment
js
function create_fragment(ctx) {
let main;
let t;
let mounted;
let dispose;
return {
c() {
main = element("main");
t = text(/*value*/ ctx[0]);
},
m(target, anchor) {
insert(target, main, anchor);
append(main, t);
if (!mounted) {
dispose = listen(main, "click", /*handleClick*/ ctx[1]);
mounted = true;
}
},
p(ctx, [dirty]) {
if (dirty & /*value*/ 1) set_data(t, /*value*/ ctx[0]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(main);
mounted = false;
dispose();
}
};
}
1.3 instance
script 标签的内容会被编译为 instance,其中需要注意:
- 只有在
UI中有渲染的变量
会被特殊编译; - value = value + 1; 变量赋值(在UI中有过渲染)将会被编译为
$invalidate(0, value = value + 1);
- instance 将会把变量值以数组的形式返回,其中
数组的下标
代表着该变量在组件中的ID值;
js
<script>
let value;
function handleClick() {
value = value + 1;
}
</script>
function instance($self, $props, $invalidate) {
let value;
function handleClick() {
// (返回的数据下标, 最新值)
$invalidate(0, value = value + 1);
}
// 返回顺序:[在UI中使用的变量, 在UI中使用的func, props];
return [value, handleClick];
}
2. 触发更新
2.1 invalidate 确认数据变更
当识别到 前后值存在变更
后,将会进行重新渲染流程;
js
$$.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)) {
// ...
make_dirty(component, i);
}
return ret;
})
: [];
2.2 make_dirty 标记脏数据
为了更新效率,框架只会更新需要的部分,svelte中不存在 虚拟dom,那么是如何记录哪些变量有变更,以及响应哪部分UI需要变更呢?
一切都在 make_dirty(component, i); 中:
js
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));
}
其中的关键代码是 component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
,Svelte利用 BitMask技术 将变动数据标识存储在 dirty 数组中;
💡 什么是BitMask掩码技术?
BitMask将多个布尔选项存储在单个整数中的技术;
假如我们有 A、B、C、D 四个值,分别为 1、2、4、8,那么我们就可以用这些值的组合表示1~15内的任意整数:
AB - 3
BD - 10
ABC - 7
ABCD - 15
前提:ABCD...的值必须是 2的次方,这样能保证每个值在二进制中都有唯一的占位;
限制:在JS中整数是带符号的 32 位整数,因为二进制最多允许31位数据存在;这其中的原理,是利用了二进制 |或运算符(两位都为0时,结果为0) 来进行存储值;
例如:AB(3)(011) | C(4)(100) = ABC(7)(111);
进一步,我们可以通过简单的 &与运算符(两位都为1时,结果为1) ,检索变量是否存在更新:
if (dirty & 1) console.log();
if (dirty & 4) console.log();
例如1:ABC(7)(111) & B(2)(010) = B(2)(010);
例如2:AC(5)(101) & B(2)(010) = 0;
从上面可以看出 BitMask 仅可以保证31位的数据存储,因此 Svelte 将 dirty 表示为数组来存储脏数据:
- 通过 (i / 31) | 0 整除算法,来判断脏数据在dirty的位置(以0~30为一个循环);
- 通过 (1 << (i % 31)) 左移算法,将脏数据转化为二进制表示(1 << 3 = 1000 = 8);
- 最后通过 &与运算符(两位都为1时,结果为1) ,来判断哪些数据有变更;
2.3 schedule_update 更新队列
在 make_dirty 中,会将当前组件存入 dirty_components 等待更新;
在 schedule_update 中,首次触发将会调用一个异步Promise,在异步循环中执行 flush更新逻辑;
js
function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
2.4 flush 循环更新
flush中将会循环遍历dirty_components,执行 update更新;
js
while (flushidx < dirty_components.length) {
const component = dirty_components[flushidx];
flushidx++;
set_current_component(component);
update(component.$$);
}
2.5 update 更新
这里主要是将脏数据数组重置,并且执行 fragment.p 更新真实DOM;
js
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);
}
}
总结
以上的探究我们看到了Svelte在 编译阶段 的探索,通过将 Svelte 将组件编译成高效的 JavaScript 代码,而不是使用运行时框架 来减少包的大小,提高应用程序性能;也看了到Svelte如何不使用无虚拟DOM,而是在编译阶段生成最小的 DOM 更新代码;但Svelte在大型项目中性能表现还待商榷,不过未来可以期待!