本文作者:nicolasxiao,腾讯前端高级工程师 MoonWebTeam成员
引言
本文基于笔者在实际项目中应用 svelte 的调研报告编辑而来,通过阅读本文,可以快速全面了解 svelte 的优缺点,社区支持,基础使用及核心原理。如果您想在实际项目中使用svelte,可以通过本文获得有力的佐证及足够信心。同时本文也提供了从基础介绍,基本使用,框架原理等多方面的资料和社区资源,方便读者快速入门。
1 svelte 是什么?
2、3年前就已经听说过 svelte 这个框架,但一直没有实际使用。 svelte 当时还是一个相对年轻的框架,只是使用在个人兴趣的项目,尝尝鲜的话还可以,但如果运用在公司内实际的项目,需要进行充分的调研,确保框架的使用成本及风险,收益。
最近一年,以个人学习的目的,浅尝过 svelte,第一印象就是框架设计得非常的清爽,写起代码来行云流水,不再需要纠结于怎么为响应式数据编写额外的代码,因为 svelte 帮你把数据响应式都做到 JS 语法里了,只需要按照原生 JS 写代码就能获得数据响应式的能力。
近期,笔者所负责的项目重构方案中选型了 svelte,并已经上线稳定运行一段时间。该项目前期立项需要快速上线,所以对技术选型采用了团队沿用下来的方案。项目开发到一定阶段,我们开始着手优化项目的性能表现,提升业务转化率,觉得有必要进行一次技术重构,既提升服务的性能指标,也提高项目的开发效率。在重构方案的设计阶段,经过多方面的调研,最终选型了 svelte。
在方案设计阶段,笔者调研了 svelte 的特性,跟其他框架在性能,实现机制,适用领域等方面的对比,在网上也翻阅了不少 svelte 相关的文章。对 svelte 的实现原理进行深入探讨的文章屈指可数,比较难以作为调研方案可行性的依据。所以只能自己动手,丰衣足食,开始着手分析 svelte 的源码,挖掘这个框架的实现机制,为调研方案提供可靠的依据。
这个阶段沉淀了不少有价值的分析,加以整理,分享出去,方便后续如果有其他团队想要选择 svelte,可以更加有信心。这就是编写本文的初衷。
至于笔者团队使用 svelte 开发的体验,给大家三个词总结:效率、性能、优雅。
那究竟是什么黑魔法,让原生的 JS 语法具备了数据响应式,本文将一步一步为您揭晓。
作者
图1 svelte 图标
相信如果读者是一个专注前端开发的同学,这些年看到的前端头条肯定少不了 svelte 的身影。诸如《都202X年了,你还没听过 svelte》此类的文章,一直在提示你,再不学 svelte 就跟不上队伍了。虽然这种介绍类的文章不少,但实际项目运用或者原理讲解的文章,则是屈指可数。
svelte 早在2016年就已经发布,这些年来不断的迭代进化,目前已经相对成熟了,官方还配套了一个开箱即用的框架 sveltekit。
它的作者是 Rich Harris,就是下图这位帅气的哥们。
图2 Rich Harris
可能大家伙儿对这位帅哥的名字比较陌生。但如果说起 rollup,大家都知道吧。没错他就是 rollup 的作者,前端届大名鼎鼎的轮子哥。大学学的是哲学,现实工作是《纽约时报》的图片编辑,这个专业跟计算机和前端八杆子打不着的人,却发明了引领前端届的数个轮子,ractive.js, svelte,还有 rollup。每一个都有不小的影响力。
编译型框架?
svelte 又是一个基于虚拟 dom 的框架吗?
自从 react,vue 之后,虚拟 dom 的概念盛行。基于虚拟 dom 技术的框架如雨后春笋般,不断的涌现。它们可能采用不同的设计模式,提供不同的接口,但本质都是一样的,通过虚拟 dom 来更新 dom 视图。
svelte 没有随大流,而是另辟蹊径使用了编译机制来实现了数据响应式。编译型框架的阵营里,除了 svelte 以外,目前还有另一个新秀——solid.js,号称目前性能最高的前端框架,在 js benchmark 上取得了仅次于原生 js 的分数。
那什么是编译型框架?编译型框架的性能为何有这种优势呢?
别急,本文后面会解答这些问题,讲解编译型框架实现原理。
2 svelte 适合实际项目吗?
前面讲到笔者已经将 svelte 运用到公司中的实际项目中,并稳定的运行了有一阵子了。在运用到实际项目前,也是在网上到处搜集 svelte 能够胜任的佐证。
经过一定时间的项目实践,svelte 表现靠谱。确认可以放心使用在实际项目。
下面我们逐一看看 svelte 的发展趋势,优点,缺点,适用场景。
趋势
从 svelte 的各项指标来看,热度还在持续的上涨。
npm trends 数据
npm:svelte - npm
发展趋势,目前稳步上涨,截止本文写作的日期,日下载当前为37.58万,虽然对比 react 和 vue 来说是少了点。但还是相当可观的,并且这条曲线一直往上攀升,相信有一天会跟主流框架还来个死亡交叉的。
图3 svelte npm trend
github 指标
github: GitHub - sveltejs/svelte: Cybernetically enhanced web apps
star 数量:
图4 svelt github star
本文第一版写作时是62500个 star,现在2周过去,涨了约600个star,当前是63100个star。关注 svelte 并予以肯定的人在不断增加。
issues 解决情况:
图5 svelte issues
目前有 689 个打开的 issue, 3973 个关闭的 issue。
大概翻阅了几页 issue,官方的开发者对 issue 相当重视,对建议,bug, 使用问题都会积极的回答。对于尝试一个相对较新的框架,网上资料还比较少时,官方的态度就很重要了。从 issue的解决情况看,官方的人还是很靠谱的。
笔者抽样统计了目前issue问题,其中包含建议(新功能建议,优化建议),使用问题(对使用方法有疑问,或者使用不当导致的误报bug),bug(主要是一些边界问题,非常用问题),bug类,有替代方案(当前框架可能有bug导致无法实现对应的功能,但可以有替代方法),已修复的bug等。
bug数非常规或致命的问题,不影响正常使用。
下面补充 svelte issue 采样统计:
issue 类型 | 问题占比 |
---|---|
建议类 | 37.5% |
使用提问类 | 29.1% |
bug类 | 16.7% |
bug类(有替代方案) | 8.3% |
bug类(已修复未关闭) | 8.3% |
引用项目数量:
图6 svelte 引用项目
github上引用了 svelte 的项目大约有12.9万个,不管是一个 hello world也好,是一个 TODO list 也好,还是一个正儿八经的项目也好,已经有不少人开始尝试 svelte 了。
stateofjs 统计数据
这是来自全球一线开发者的统计数据,具有一定的参考价值。
图7 stateofjs 数据
根据统计,94%的前端开发者听说过 svelte。90%的开发者有了解过这个框架,并持续保持关注。对 svelte 感兴趣的开发者占 68%,位列第一。
从这份数据上看,大部分前端开发者有听过 svelte,有三分之二的开发者对其感兴趣。看起来,大家还是很看好这个框架。
优点
高性能
svelte 作为一个编译型的前端框架代表,它将需要在运行时做的事情,提前到编译阶段完成,所以它几乎没有运行时。它的运行时主要是工具函数,辅助进行dom的更新,任务的调度等。运行阶段无需处理依赖收集,虚拟 dom 比较等额外计算,所以性能自然而然会有先天的优势。
比如依赖收集,svelte 在编译阶段已经提前计算好哪个变量会在哪里引用,需要在什么时候更新 DOM,并且生成了具体的 DOM 更新指令,运行时通过对变量进行脏标记,根据脏标记更新 DOM 视图。举个反例: 像某些需要运行时收集依赖的框架,需要在模板渲染时,或者是计算属性被 evaluate 时,才开始进行依赖的收集,这无疑增加了代码执行的耗时。
再比如,svelte 是不需要虚拟 dom 的,它在编译阶段直接生成创建 dom,更新 dom 的过程式代码。而基于虚拟 dom 的框架,则需要在每次数据更新时,重新生成虚拟 dom,并对新旧两个虚拟 dom 树进行比较,最后才能把改变更新到真实的 dom 上。
正因为 svelte 把框架的抽象都从运行时前移到了编译期进行处理,提前分析依赖,生成脏检查语句,生成 dom 的 patch 代码等,去除了运行时的依赖分析,虚拟 dom 等计算耗时,减少了运行时的负担,又在底层实现充分考虑了性能,例如用位运算做数据变更的标记,让整个框架变得很高效。
产物体积小
svelte 框架的运行时非常小,仅仅 18K,在组件数量不多的场景下,其构建产物要明显优于 vue3,react等框架。很适合轻量级的项目。针对这个优势,也有相关评测指出,随着 svelte 组件数量的增多,运行时体积的优势将会被组件拖垮,一般组件数量不超过19个, svelte 产物体积会优于 vue3 。(信息来源 vue 作者尤大大)
心智负担低
svelte 相较于其他框架,实现相同的功能需要编写的代码更简洁。
svelte 通过编译机制让原生 javascript 支持数据响应式。这种基于编译机制,对于开发者而言是完全透明的。
打个比方:
下面是 svelte 通过数据来更新视图的例子:
<div on:click={handleClick}>
{message}
</div>
let message = ''; // 1.声明变量
const handleClick = () => message = 'hello'; // 2.数据响应
在代码第六行处, message = 'hello',这是一句普通的 javascript 赋值语句。但在 svelte 的编译处理下,这个语句新增了数据响应式的语义。当变量发生赋值时, svelte 会帮忙处理好数据的响应式,更新视图等操作。
如果没有在编译阶段对语义进行处理,单靠运行时绝对是没法实现的。
我们可以看看其他框架的妥协做法:
比如 vue3
<template>
<div @click="handleClick">
{{message}}
</div>
</template>
<script setup>
const message = ref(''); // 1.声明变量
const handleClick = () => message.value = 'hello'; // 2.数据响应
</script>
由于没有编译器的预处理,vue3只能靠运行时,给变量做封装。message 已经不是一个单纯的 javascript 字符串变量,而是一个对象。这些为数据响应式添加的机制,无疑增加了心智负担。开发者不是在写 plain javascript,尽管框架尽力往原生语法的体验靠拢,但本质上还是在对框架调用各种接口。
正因为 svelte 在编译阶段语义处理上添加框架的特性,保持了 javascript 原来的模样,开发者不需要有心智负担,不会被各种写法搞的焦头烂额。既不容易用错,也不需要浪费太多的精力去学习一个框架各种约定的规则。
丰富的特性
图8 svelte 官网特性展示
现在前端框架该有的 feature, svelte 一个都没有落下。
数据响应式,computed属性,双向绑定,事件透传,一应俱全。
甚至,svelte 把 store 也放到框架里,真正做到开箱即用。
上手简单
svelte 把框架代码编写风格设计得跟 HTML 文件规范几乎一模一样。
编写一个 svelte 组件的体验,跟开发原生 web 基本相同:写 HTML 文档结构,在 script 标签内编写 js 代码,在style 标签内编写样式。
这种方式对于初学者很友好,只需要知道如何编写网页,就可以平稳的过渡到 svelte 。学习成本很低。
额外需要关注的扩展并不多,这里我提炼了一下:
- 赋值语句能触发数据响应式
- 使用 $: 可以声明计算属性
- 使用 $ + store 的变量名可以实现 store 的订阅
只要记住上面三个规则,再加上一些基础的 HTML 网页开发技术,就能快速上手 svelte。
灵活
如果用 svelte 开发一个组件,外部调用可以把这个组件当作一个用 js 写的类来使用,直接通过 new 来创建组件,通过实例方法来调用组件的方法,非常实用。
可以看看下面的例子:
// App 是一个 svelte 编写的组件
import App from './App.svelte';
// 这里把 App 当做类进行实例化就能创建出组件的实例
const app = new App({
target: document.body,
props: {
answer: 42
}
});
另外,svelte 还提供了 web component 的支持,可以通过修改编译选项,将 svelte 写的组件编译成 web component。有了 web component,甚至可以在原生 js ,vue ,react等其他框架中使用 svelte编写的组件。关于 svelte 开发 web component,后面笔者会单独写一篇文章介绍。
缺点
编译产物代码冗余
svelte 编译输出的组件代码相较于 vue,react 等框架还是稍微冗长了些。
比如编写一个很简单的组件如下:
<h1>Hello world!</h1>
会生成如下的 js 代码:
/* App.svelte generated by Svelte v3.52.0 */
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;
可以看看 react jsx的构建例子:
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
通过 jsx 编译后的产物:
React.createElement(
MyButton,
{color: 'blue', shadowSize: 2},
'Click Me'
)
svelte 生成的是命令式的dom创建过程,虚拟 dom 的框架生成的是虚拟 dom 结构创建的过程(vdom 渲染函数)。在基于虚拟 DOM 的框架里,虚拟dom到真实dom的转换过程,被封装在运行时里,所以每个组件虚拟 dom 创建过程仅仅是数据结构的表述,更为紧凑,代码产物也就比较少。
生态不够成熟
svelte 诞生到现在有6年的时间,虽然已经有一定数量的使用者,但大公司使用的案例还是比较少。这也导致大家对这个新兴框架敬而远之。
svelte 周边的类库还不够完善,比如想找一个像 ant-design 这样成熟的组件库,目前还是没有的,只能找到一些比较轻量级的组件库。
中文相关的文章也比较匮乏。
英文社区的文档和视频会稍微好一些。
生态不够成熟确实是比较大的问题,导致我们使用 svelte 需要重复造一些轮子,对于某些需要现成组件的项目研发启动的速度会偏慢。每一个新兴的框架其实都需要经历这个过程,随着越来越多的人加入,生态会越来越好的。
适用场景
基于 svelte 高性能,产物体积小等优点, svelte 很适合开发移动端 H5 的运营营销活动。目前我们也是将 svelte 运用到一个大型的活动页面,并充分运用 svelte 的各种特性,目前项目已经上线,首屏的性能指标提升明显,且暂未遇到难以解决的坑点。
3 svelte 的基本使用
学习每个新的语言和框架,免不了一个 Hello World。下面从一个 Hello World 例子展开,以 svelte store 结尾。看完写一个增删改查的 TODO list 应该不在话下。
另,在 svelte 官网有详细的教程:
Introduction / Basics • Svelte Tutorial
3.1 svelte 脚手架
创建 svelte 项目有三种方式:手动创建,vite 脚手架,sveltekit 脚手架
这里首选推荐 vite 脚手架或者 sveltekit 脚手架,除非项目有较多定制化打包需求才选用手动创建项目的方式。
vite 脚手架
通过 vite 创建 svelte js 项目:
npm create vite@latest myapp -- --template svelte
如果使用 typescript ,可以更换 svelte-ts 模板
npm create vite@latest myapp -- --template svelte-ts
sveltekit 脚手架
sveltekit 脚手架提供交互式的选项,可以定制项目的语言,测试,eslint等配置,相对 vite 脚手架,更为全面。
npm create svelte@latest my-app
手动创建
手动配置需要配置打包工具,测试工具,lint 工具等
首选的打包工具 vite(svelte 官方对 vite 支持最好), 当然 webpack 和 rollup 也有对应的 svelte 方案。
npm install -D @sveltejs/vite-plugin-svelte
npm install -D svelte-preprocess
npm install -D eslint-plugin-svelte
vite.config.ts
import { svelte } from '@sveltejs/vite-plugin-svelte';
import sveltePreprocess from 'svelte-preprocess';
export default defineConfig(({ mode }) => ({
plugins: [
svelte({
preprocess: [sveltePreprocess({ typescript: true })],
}),
],
}));
.eslintrc
{
"extends": [
"plugin:svelte/base",
],
"parserOptions": {
"parser": "@typescript-eslint/parser",
"ecmaVersion": 2020,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
},
"extraFileExtensions": [
".svelte"
]
},
"plugins": [
"@typescript-eslint"
],
"overrides": [
{
"files": [
"*.svelte"
],
"parser": "svelte-eslint-parser",
"parserOptions": {
"parser": "@typescript-eslint/parser"
}
}
],
}
需要特别注意,官方开发的 eslint 插件对 typescript 支持有问题,推荐使用下面这个 eslint 插件,支持非常完美,作者也是 svelte, vue3 的贡献者 Yosuke Ota⬇️
GitHub - ota-meshi/eslint-plugin-svelte: ESLint plugin for Svelte using AST
3.2 svelte REPL
如果只是想学习 svelte,可以不急着在本地搭建 svelte 的开发环境。官方为我们提供了 REPL。可以在 REPL 编写 svelte 代码并实时查看结果。REPL 很适合学习入门,或者需要编写 DEMO 验证功能时使用。
点击下方链接直达 svelte REPL ⬇️
3.3 Hello, Svelte
svelte 的程序结构分为三部分: 模版(template),脚本(script),样式(style)
与 HTML 语法结构高度一致
与 HTML是,在 script 里声明的所有变量,都可以在模版中引用。同时样式是局部的(scoped),只会应用在当前的模板
<div>
Hello, {name}!
</div>
<script>
const name = 'Svelte';
</script>
<style>
div {
color: orange;
}
</style>
放入 REPL 可以看到 svelte 输出了一个橙色的"Hello, Svelte! "
图9 Hello, Svelte
3.4 事件绑定
svelte 的事件绑定使用 on:事件名 的格式,如下代码所示
<button on:click={handleClick}>click me</button>
下面的例子演示当用户点击按钮,浏览器将弹出 Clicked!的信息
<button on:click={showMessage}>click me</button>
<script>
const showMessage = () => alert('Clicked!');
</script>
放到 svelte REPL 运行得到如下结果:
图10 事件绑定
3.5 赋值
每个前端框架在数据驱动视图的方式上都各显神通,比如 vue2 利用 getter setter的数据响应式,又或者是 vue3 使用 proxy 实现的,再比如 react 的 hooks。
svelte 采用的是编译方式:对变量赋值语句生成额外的数据响应式逻辑。
只要在 javascript 里有对变量赋值,就会自动触发数据的响应式。不需要多余的 api 调用。
可以用下面的例子对比下 vue3 和 svelte
两个例子都是实现了“点击按钮,修改按钮文本”的逻辑
vue3 版本:
<template>
<button @click="handleClick">{{title}}</button>
</template>
<script setup>
const title = ref("click me!");
const handleClick = () => {
title.value = "you clicked me!";
}
</script>
vue3 里响应式数据需要用 ref 来封装。赋值需要通过.value才能实现响应式。而模板里,可以省略 .value。
svelte 版本:
<button on:click={handleClick}>{title}</button>
<script>
let title = 'click me!';
const handleClick = () => {
title = 'you clicked me!';
}
</script>
先放到 REPL 里看看效果
图12 数据响应式
按钮确实更新了。
但代码里只有对变量的赋值,不需要 ref,.value 或者类似 setState 之类的数据更新机制。
可通过上面例子看到 svelte 里变量赋值自带了响应式。
但是翻遍 JS 的语法特性,肯定找不到这样的特性的。
别急,本文第四节会深入 svelte 的底层机制,解密 svelte 数据响应式的原理。
当进行数组操作,如push,splice, unshift等,因为不满足响应的数据放在等号的左侧的原则,我们需要多写一点代码,来触发svelte的响应式:
let todos = []
function addTodo() {
todos = [...todos, 'new todo'] // 有等号,会触发svelte的响应式
}
3.6 神奇的 $ 符号
svelte使用一个特定的语法来表达,在赋值表达式前加上$:定义计算属性
$: numOfTodos = todos.length
等价于 vue 的 computed:
const numOfTodos = computed(() => todos.length)
$: 还可以实现 vue watchEffect 的效果
$: {
if (!numOfTodos) {
console.log('todos is empty')
}
}
3.7 麻雀虽小,五脏俱全 - svelte store
svelte 居然还包含了一个 store 的实现。
svelte store 的设计很简洁,下面以一个 svelte 官方的 custom store 的例子展示 svelte store 的用法。(这也是我们实际项目用得最多的形式)
stores.js:
function createCount() {
const { subscribe, set, update } = writable(0);
return {
subscribe,
increment: () => update(n => n + 1),
decrement: () => update(n => n - 1),
reset: () => set(0)
};
}
app.svelte:
<script>
import { count } from './stores.js';
</script>
<h1>The count is {$count}</h1>
<button on:click={count.increment}>+</button>
<button on:click={count.decrement}>-</button>
<button on:click={count.reset}>reset</button>
-
writeable 用于创建一个 svelte store 实例。
-
store 实例方法 subscribe 用于 store 改动的订阅,实际使用常常被 $store 这种简写代替
-
set 用于修改 store 的值
-
update 用于更新 store 的值
4 svelte 的核心实现
前面一章介绍了 svelte 的用法,通过 js 的赋值语法,能触发数据的响应式逻辑,进而更新视图。想必读者首次看到这种黑科技,估计脑海里会把 defineProperty,getter,setter,proxy都遍历一般,这是 javascript 的新特性吗?怎么把数据响应式都做到语言特性里了?
为了更好的发挥 svelte 的优势,更快的定位解决实际使用问题,有必要对 svelte 的原理进行深入的探究。下文将对 svelte 的核心机制进行剖析。
4.1 编译型前端框架
我们来看看 vue 作者尤大大前些年对 svelte 的评价:
"That would make it technically SvelteScript, right?"
图13 Rich 的演讲
这句话是想表达:svelte 是造了个编译器吗?
确实可以理解成为 svelte 给 javascript 的编译器做了魔改。
在 svelte 源码里,使用了 acorn 将 javascript 编译成 ast 树,然后对 javascript 的语义解释过程做了额外的工作:
-
编译赋值语句时,除了生成对应的赋值逻辑,额外生成数据更新逻辑代码
-
编译变量声明时,变量被编译成上下文数组
-
编译模板时,标记依赖,并对每个变量引用生成更新逻辑
这就是编译型框架,与传统前端框架的区别:把运行时的逻辑提前在编译期就完成。所以自然而然的,运行时逻辑很轻量级,很显然是有利于页面的首屏和渲染性能的。
4.2 实现原理
本节将会从 svelte 的组件底层实现,各种模板语句的编译,svelte 的脚本编译等原理分别展开讲解。
4.2.1 组件的底层实现
每一个 .svelte 文件代表一个 svelte 的组件。
通过 svelte 的编译,最终会转换为下图所示的组件的结构
图14 Svelte 组件底层结构
每一个 svelte 的组件类,都继承了SvelteComponent。
svelte 组件使用create, mount, patch, destroy 这四个方法实现对 DOM 视图的操作。
-
create 负责组件dom的创建
-
mount 负责将 dom 挂载到对应的父节点上
-
patch 负责根据数据的变化更新 dom
-
destroy 负责销毁对应的 dom
svelte 的组件实例化,是通过 instance 方法和组件上下文构成的。
- instance 方法:可以理解为 instance方法是 svelte 组件的构造器。写在 script 里的代码,会被生成在 instance 方法里。每个组件实例都会调用一次形成自己的闭包,从而隔离各自的数据,通过 instance 方法返回的数组就是上下文。代码中的赋值语句,会被生成为数据更新逻辑。变量定义会被收集生成上下文数组。
- 上下文:每个 svelte 组件都会有自己的上下文,上下文存储的就是 script 标签内定义的变量的值。svelte 会为每个组件实例内定义的数据生成上下文,按照变量的声明顺序保存在一个名为 ctx 数组内。
图15 上下文结构
4.2.2 模板编译
4.2.2.1 视图的创建
前端框架创建视图的方式有几种,比如虚拟 dom,字符串模板,过程式创建。
svelte 采用的是过程式创建。
举个例子,假设我想要通过纯 js 的方式创建一个如下的 web ui:
图16 web demo
我们可能会写下这样的代码:
const todoListNode = document.createElement('ul');
const todos = [1,2,3];
for (const todo of todos) {
const itemNode = document.createElement('li');
itemNode.textContent = `item ${todo}`;
todoListNode.appendChild(itemNode);
}
而 svelte 生成的视图代码就很类似我们手动编写的 js 代码。
这部分创建 dom 的代码,会生成为组件内部的 create 函数, mount 函数,patch 函数。
下面我们来看一下模板编译过程。
-
首先解析 svelte 模板并生成模板 AST
-
然后遍历模板 AST
-
- 如果碰到普通的 html tag 或者文本,输出 dom 创建语句(dom.createElement)
- 如果碰到变量
-
- 转换为上下文引用方式并输出取值语句(如: name 被生成为 ctx[/** name */0])
- 在 patch 函数中生成对应的更新语句
- 如果碰到 if 模板
-
- 获取 condition 语句,输出选择函数 select_block (子模板选择器)
- 获取 condition 为 true 的模板片段,输出 if_block 子模板构建函数
- 获取 condition 为 false 的模板片段,输出 else_block 子模板构建函数
- 如果碰到 each 模板
-
- 获取循环模板片段,生成块构建函数 create_each_block
- 根据循环内变量引用,生成循环实例上下文获取 get_each_block_context
-
生成 key获取函数 get_key
-
生成基于key更新列表的patch逻辑函数 update_keyed_each
图17 模板AST
子模板构建函数
svelte 会把 if 模板, each 模板中的逻辑分支,抽取成子模板,并为其生成独立的模板实例(包含创建,挂载,更新,销毁等生命周期)
4.2.2.2 视图更新
视图更新时通过patch函数来完成的。
下图是模板解析过程中patch函数的逻辑:
function patch(ctx, [dirty]) {
if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]);
if (dirty & /*age*/ 2) set_data(t4, /*age*/ ctx[1]);
if (dirty & /*school*/ 4) set_data(t6, /*school*/ ctx[2]);
}
通过 dirty 位检查变量是否发生更新,如果发生更新调用 dom 操作函数对 dom 进行局部更新。上面例子的 set_data 函数作用是给 dom 设置 innerText。根据数据更新的视图位置的不同,还会有 set_props之类的更新 dom 属性的函数等。
4.2.2.3 条件分支的处理
条件分支例子:
<script>
let isLogin = false;
const login = () => {
isLogin = true;
}
const logout = () => {
isLogin = false;
}
</script>
{#if !isLogin}
<button on:click={login}>
login
</button>
{:else}
<div>
hello, xxx
<button on:click={logout}>logout</button>
</div>
{/if}
图18 if 模板产物
- 条件分支的判断语句会生成select block函数,用于判断条件,并根据条件返回条件判断为真的子模板(if_block)或者条件判断为假的子模板(else_block)
// 根据条件返回对应的block构造函数
function select_block(ctx, dirty) {
if (!/*isLogin*/ ctx[0]) return if_block;
return else_block;
}
// 选择block构造函数
let current_block = select_block(ctx, -1);
// 返回子模板实例,跟组件类似,提供create,mount,patch等生命周期
let block = current_block(ctx);
- 条件逻辑分支会生成独立的子模板构造函数
if block示例
// 子模板构造函数
function if_block(ctx) {
let button;
let mounted;
let dispose;
return {
// 创建block
create() {
button = element("button");
button.textContent = "login";
},
// 挂载block
mount(target, anchor) {
insert(target, button, anchor);
if (!mounted) {
mounted = true;
}
},
// 销毁block
destroy(detaching) {
if (detaching) detach(button);
mounted = false;
dispose();
}
};
}
- if分支如何挂载及更新
if 分支的创建:
图19 if 分支创建逻辑
if 分支的更新:
图20 if 分支更新逻辑
4.2.2.4 循环模板的处理
svelte的循环模板跟条件分支模板一样,也会生成迭代逻辑的子模板,每一个循环迭代都是子模板的实例,并且拥有独立的上下文。
主要由4部分组成:
-
循环迭代构建函数 create_each_block
-
循环迭代实例上下文获取函数 get_each_block_context
-
循环迭代 key 获取函数 get_key
-
基于 key 更新列表的 patch 逻辑函数 update_keyed_each
4.2.3 脚本编译
4.2.3.1 编译过程
-
svelte 调用 acorn 生成 JS AST 树
-
遍历 AST 找到赋值语句
-
为赋值语句生成数据响应式
图21 赋值语句编译流程
svelte 组件源码:
<script>
let name = 'world';
const changeName = () => {
name = 'yyb';
}
</script>
编译结果:
function instance($$self, $$props, $$invalidate) {
let name = 'world';
const changeName = () => {
$$invalidate(0, name = 'yyb');
};
return [name];
}
4.2.3.2 $$invalidate
每个数据的赋值语句,svelte都会生成对invalidate的调用主要做的是对某个改动的变量进行标记,然后在微任务中调用patch函数,根据变量改动的脏标记进行局部更新
数据赋值触发视图更新:
图22 赋值触发视图更新逻辑
4.2.3.3 dirty 脏标记
svelte 通过位运算(bitmask)对变量的改变进行脏标记
每个变量都被分配一个位值,可以用于在 ctx 上下文数据里取得变量对应的值,也可以通过位运算对变量改动进行标记和检查。
比如 name 的位值是 1,那 name 的值可以通过 ctx[1]取得。
通过 dirty |= 1 设置 name 已经改动的状态,再通过 dirty & 1 判断 name 是否改动。
按 javascript 的位运算可以有 32 位。 svelte 支持每个组件里对 32 个变量标记改动。
一般一个组件不应该定义过多的变量。当然如果定义变量多于 32 个,无非就是拿两个位标记变量,凑成 64 位,以此类推。
图23 脏标记结构
设置位:
bitmask |= 1 << (n-1)
检测位:
if (bitmask & (1 << (n-1)))
变量 | 位置 | 位值 |
---|---|---|
name | 1 | 1<<(1-1)=1 |
age | 2 | 1<<(2-1)=2 |
school | 3 | 1<<(3-1)=4 |
5 总结
本文汇总了笔者调研 svelte 实际项目运用的可行性信息,收益,坑点,同时整理了部分笔者分析 svelte 运行机制的分析案例。相信还有不少遗漏的地方,后续有时间会继续深入 svelte 源码,给大家分享更多细节。
这段时间通过公司内部几个前端项目对 svelte 的实践,既有完全开荒的新项目,也有存在历史包袱的老项目。过程中感受的是现阶段的 svelte 已经相当成熟,开发过程中遇到的问题,基本可以通过官方文档,社区找到解决方案。整体的体验是很顺滑的。 svelte 基于编译技术实现响应式的设计理念也给笔者不小的惊艳。
最终的期望大家多了解 svelte 这个框架,别再 《都202X年了,还没听过 svelte》了,感兴趣就加入 svelte 阵营。相信 svelte 会在各方面不断带给你惊喜。
参考资料
GitHub - feltcoop/why-svelte: Why Svelte is our choice for a large web project in 2020