Svelte runtime 源码浅析原创
一、什么是 Svelte
❝
Svelte 是一种全新的构建用户界面的方法。传统框架如 React 和 Vue 需要在「浏览器」中做大量的工作,而 Svelte 将这些工作转移到构建应用程序的「编译阶段」来处理。
不同于使用虚拟 DOM 进行差异对比,Svelte 编写的代码会在应用状态变更时像做外科手术一样精准更新 DOM。
❞
二、Svelte 的特点
1、 代码量少
我们分别用 React、Vue、Svelte 写一个相同功能的 count 变量的计数器。
React
import { useState } from 'react'
function App() {
const [count, setCount] = useState(0)
const handleClick = () => {
setCount(count+1)
}
return (
<main>
<button onClick={handleClick}>点击</button>
<div>{count}</div>
</main>
)
}
export default App
Vue
<script setup>
import { ref } from '@vue/reactivity'
let count = ref(0)
const handleClick = () => {
count.value += 1
}
</script>
<template>
<main>
<button @click='handleClick'>点击</button>
<div>{{count}}</div>
</main>
</template>
Svelte
<script>
let count = 0
function handleClick() {
count += 1
}
</script>
<main>
<button on:click={handleClick}>点击</button>
<div>{count}</div>
</main>
代码量分别为14,13,10行,显而易见 Svelte 代码量是最少的,而且基本没什么特殊语法。
2、无虚拟 DOM
Svelte 在 compiler 阶段会将代码直接编译成框架无关的纯 js 代码,执行效率更高,而 Vue 和 React 都需要经过一层虚拟 DOM。
3、真正的响应式
无需复杂的状态管理库,Svelte 使用 js 语言本身的能力实现了响应式,没有额外的心智模型,更加直观易用。
4、产物体积更小?
由于 Svelte 减少了 runtime 层,编译后的产物体积会更小,不过这个并不绝对,随着应用组件数量的增加,Svelte 的体积会更快的上升。
5、性能更好?
Svelte 舍弃了虚拟 DOM,并且执行的是纯 js 代码,在性能测试中的表现会相对优异,不过这点同样受应用规模的影响。
三、runtime 源码浅析
1、调试步骤
执行 npx degit sveltejs/template svelte-demo 脚本新建项目,然后安装依赖
克隆 svelte 仓库,安装依赖、运行。执行 npm link 注册软链接
在 svelte-demo 中执行 npm link svelte 执行软链接
2、修改 App.svelte 代码如下
<script>
let count = 0
function handleClick() {
count += 1
}
</script>
<main>
<button on:click={handleClick}>点击</button>
<div>{count}</div>
</main>
将代码放在 svelte 在线代码编译模板(https://www.sveltejs.cn/examples#hello-world)中,查看编译结果如下
3、分析编译后的代码
代码为函数 create_fragment、函数 instance、App 类三部分。最下面的 App 类为最终导出的结果,可以看到主要是将 instance 和 create_fragment 函数传入构造函数中,然后调用 init 函数,构造 App 对象。
class App extends SvelteComponent {
constructor(options) {
super()
init(this, options, instance, create_fragment, safe_not_equal{})
}
}
export default App
接下来看 instance 源码。
function instance($$self, $$props, $$invalidate) {
let count = 0
function handleClick() {
$$invalidate(0, count += 1)
}
return [count, handleClick]
}
instance 函数返回了一个数组,元素为我们定义的 count 和 handleClick,代码比较简单,其中在 handleClick 函数中调用了 instance 的参数 $$invalidate。
$$invalidate 为一个函数,第一个参数是当前变量在 instance 模板的位置,第二个参数为更新的方法。
接下来看 create_fragment 函数。
function create_fragment(ctx) {
let main;
let button;
let t1;
let div;
let t2;
let mounted;
let dispose;
return {
c() {
main = element("main");
button = element("button");
button.textContent = "点击";
t1 = space();
div = element("div");
t2 = text(/*count*/ ctx[0]);
attr(main, "class", "svelte-1tky8bj");
},
m(target, anchor) {
insert(target, main, anchor);
append(main, button);
append(main, t1);
append(main, div);
append(div, t2);
if (!mounted) {
dispose = listen(button, "click", /*handleClick*/ ctx[1]);
mounted = true;
}
},
p(ctx, [dirty]) {
if (dirty & /*count*/ 1) set_data(t2, /*count*/ ctx[0]);
},
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(main);
mounted = false;
dispose();
}
};
}
可以看到函数主要是和 DOM 操作相关的,我们接下来简单分析一下。
-
create_fragment 入参为 ctx,即上面 instance 函数执行后返回的数组(里面元素为声明的变量和函数)。
-
create_fragment 返回一个对象,对象中的不同函数对应页面加载的不同阶段。
-
c 为 create 函数,分别将 svelte 模板的每一段节点块创建一个变量标签(创建真实 DOM)。
-
m 为 mounted 函数,将每一个变量标签添加到父 DOM 节点中,并且调用 listen 方法给 button 添加 DOM 的 click 事件,函数为 ctx[1] 即 handleClick(插入真实 DOM)。
-
p 为 update 函数,调用 set_data,将 ctx[0] 即 count 的变量(真实 DOM 的赋值)DOM 中更新最新的变量值。
-
d 为 destory 函数,调用 detach 函数销毁 DOM 并且将 mounted 赋值为 false,并且调用 dispose 函数移除事件的监听。
4、 分析 runtime 中的代码
由上面可以看到 compile 中的 App 类最终会调用 init 方法。
在源码中 svelte/src/runtime/internal/Component.ts 中第109行,把处理边缘 case 的代码精简后如下。
export function init(component, options, instance, create_fragment, not_equal, props, append_styles, dirty = [-1]) {
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(options.context || (parent_component ?
parent_component.$$.context : [])),
// everything else
callbacks: blank_object(),
dirty,
skip_bound: false,
root: options.target || parent_component.$$.root
};
// 调用传入instance的函数将返回的变量和方法数组赋值给$$.ctx变量,同时在instance函数中接收
// 组件更新后的方法,将更新后的新值传给make_dirty函数进行更新
$$.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;
}) : [];
// 调用before_update函数
run_all($$.before_update);
// 调用create_fragment函数将返回的真实Dom赋值给$$.fragment,入参为$$.ctx变量
$$.fragment = create_fragment ? create_fragment($$.ctx) : false;
// 调用组件的create方法
$$.fragment && $$.fragment!.c();
//调用组件的mounted方法
mount_component(component, options.target, options.anchor, options.customElement);
// 刷新视图,更新页面
flush();
}
5、Svelte 整个流程简图
- Svelte 在编译时将组件中的变量和方法封装在 instance 函数中,返回值为一个数组。将组件中的 DOM 和生命周期封装在 create_fragment 函数中。然后调用运行时的 init 方法将 instance 和 create_fragment 传入。
- 当组件点击时将调用 instance 函数返回值中对应的事件,将组件中变量的下标和最新值返回并且调用 init 中的回调函数。
- 在 init 的回调函数中调用 make_dirty 标记需要更新的值。
- 然后进行视图刷新后调用 create_fragment 函数中的 update 函数将对应的值更新到 DOM 中。
总结
Svelte 在编译阶段将大部分准备工作做好,在运行时候只需要进行脏检查和视图刷新,调用编译好的组件的更新函数即可。这种设计思路在当前前端环境下还是非常有新意的,在开发者中的评价也比较高,大家有兴趣可以尝试一下。
最后,简单总结一下:
「优点」:上手简单,使用起来心智负担低;代码简洁;开发效率高;性能好。
「缺点」:生态不完善,组件库少;组件过多时体积会膨胀;大型复杂应用的实践较少。
我们是数数科技前端团队,目前负责游戏行业使用最多的用户行为分析系统的前端研发,同时也在积极探索前端新技术和新领域,如果你对游戏、大数据、可视化、工程化、全栈等方面有兴趣,欢迎加入我们,共创未来!