一文讲透前端新秀 svelte

4,286 阅读23分钟

本文作者: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 。学习成本很低。

额外需要关注的扩展并不多,这里我提炼了一下:

  1. 赋值语句能触发数据响应式
  2. 使用 $: 可以声明计算属性
  3. 使用 $ + 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 ⬇️

Hello world • REPL • Svelte

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 模板产物

  1. 条件分支的判断语句会生成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);
  1. 条件逻辑分支会生成独立的子模板构造函数

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();
    }
  };
}
  1. if分支如何挂载及更新

if 分支的创建:

图19 if 分支创建逻辑

if 分支的更新:

图20 if 分支更新逻辑

4.2.2.4 循环模板的处理

svelte的循环模板跟条件分支模板一样,也会生成迭代逻辑的子模板,每一个循环迭代都是子模板的实例,并且拥有独立的上下文。

主要由4部分组成:

  1. 循环迭代构建函数 create_each_block

  2. 循环迭代实例上下文获取函数 get_each_block_context

  3. 循环迭代 key 获取函数 get_key

  4. 基于 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的调用,invalidate的调用,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)))

变量位置位值
name11<<(1-1)=1
age21<<(2-1)=2
school31<<(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

The State of JS 2021: Front-end Frameworks