zl程序教程

您现在的位置是:首页 >  前端

当前栏目

一文讲透前端新秀 svelte

前端 一文 讲透 新秀 Svelte
2023-06-13 09:15:23 时间

本文作者:nicolasxiao,腾讯前端高级工程师

引言

本文基于笔者在实际项目中应用svelte的调研报告整理而来,实际项目中,通过将 vue3 替换成 svelte,框架体积就从337.46kb减少到18kb,页面性能指标提升了57%。通过阅读本文,可以快速全面了解 svelte 的优缺点,社区支持,基础使用及核心原理。如果您想在实际项目中使用svelte,可以通过本文获得有力的佐证及足够信心。

1 svelte 是什么?

2、3年前就已经听说过 svelte 这个框架,但一直没有实际使用。svelte 当时还是一个相对年轻的框架,只是使用在个人兴趣的项目,尝尝鲜的话还可以,但如果运用在公司内实际的项目,需要进行充分的调研,确保框架的使用成本及风险,收益。

最近一年,以个人学习的目的,浅尝过 svelte,第一印象就是框架设计得非常的清爽,写起代码来行云流水,不再需要纠结于怎么为响应式数据编写额外的代码,因为 svelte 帮你把数据响应式都做到 JS 语法里了,只需要按照原生 JS 写代码就能获得数据响应式的能力。

近期,笔者所负责的项目重构方案中选型了 svelte,并已经上线稳定运行一段时间。该项目前期立项需要快速上线,所以对技术选型采用了团队沿用下来的方案。项目开发到一定阶段,我们开始着手优化项目的性能表现,提升业务转化率,觉得有必要进行一次技术重构,既提升服务的性能指标,也提高项目的开发效率。在重构方案的设计阶段,经过多方面的调研,最终选型了 svelte。

在方案设计阶段,笔者调研了svelte的特性,跟其他框架在性能、实现机制、适用领域等方面的对比,在网上也翻阅了不少  svelte  相关的文章。对 svelte 的实现原理进行深入探讨的文章屈指可数,比较难以作为调研方案可行性的依据。所以只能自己动手,丰衣足食,开始着手分析 svelte 的源码,挖掘这个框架的实现机制,为调研方案提供可靠的依据。

这个阶段沉淀了不少有价值的分析,加以整理,分享出去,方便后续如果有其他团队想要选择  svelte,可以更加有信心。这就是编写本文的初衷。

至于笔者团队使用  svelte  开发的体验,给大家三个词总结:效率、性能、优雅。

那究竟是什么黑魔法,让原生的  JS  语法具备了数据响应式,本文将一步一步为您揭晓。

1.1、作者

相信如果读者是一个专注前端开发的同学,这些年看到的前端头条肯定少不了 svelte 的身影。诸如《都202X年了,你还没听过 svelte》此类的文章,一直在提示你,再不学 svelte 就跟不上队伍了。虽然这种介绍类的文章不少,但实际项目运用或者原理讲解的文章,则是屈指可数。

svelte 早在2016年就已经发布,这些年来不断的迭代进化,目前已经相对成熟了,官方还配套了一个开箱即用的框架 sveltekit。

它的作者是 Rich Harris,就是下图这位帅气的哥们。

图2 Rich Harris

可能大家伙儿对这位帅哥的名字比较陌生。但如果说起 rollup,大家都知道吧。没错他就是 rollup 的作者,前端届大名鼎鼎的轮子哥。大学学的是哲学,现实工作是《纽约时报》的图片编辑,这个专业跟计算机和前端八杆子打不着的人,却发明了引领前端届的数个轮子,ractive.js, svelte,还有 rollup。每一个都有不小的影响力。

1.2、编译型框架?

 svelte 又是一个基于虚拟 dom 的框架吗?

自从 react,vue 之后,虚拟  dom  的概念盛行。基于虚拟 dom 技术的框架如雨后春笋般,不断的涌现。它们可能采用不同的设计模式,提供不同的接口,但本质都是一样的,通过虚拟  dom  来更新  dom  视图。

svelte 没有随大流,而是另辟蹊径使用了编译机制来实现了数据响应式。编译型框架的阵营里,除了 svelte 以外,目前还有另一个新秀——solid.js,号称目前性能最高的前端框架,在 js benchmark 上取得了仅次于原生 js 的分数。

那什么是编译型框架?编译型框架的性能为何有这种优势呢?

别急,本文后面会解答这些问题,讲解编译型框架实现原理。

2  svelte 适合实际项目吗?

前面讲到笔者已经将 svelte 运用到公司中的实际项目中,并稳定的运行了有一阵子了。在运用到实际项目前,也是在网上到处搜集 svelte 能够胜任的佐证。

经过一定时间的项目实践,svelte 表现靠谱。确认可以放心使用在实际项目。

下面我们逐一看看 svelte 的发展趋势,优点,缺点,适用场景。

2.1、趋势

从 svelte 的各项指标来看,热度还在持续的上涨。

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,有三分之二的开发者对其感兴趣。看起来,大家还是很看好这个框架。

2.2、优点

高性能

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,后面笔者会单独写一篇文章介绍。

2.3、缺点

编译产物代码冗余

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 需要重复造一些轮子,对于某些需要现成组件的项目研发启动的速度会偏慢。每一个新兴的框架其实都需要经历这个过程,随着越来越多的人加入,生态会越来越好的。

2.4、适用场景

基于 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-sveltenpm install -D svelte-preprocessnpm 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>

lwriteable 用于创建一个 svelte store 实例。

lstore 实例方法 subscribe 用于 store 改动的订阅,实际使用常常被 $store 这种简写代替

lset 用于修改 store 的值

lupdate 用于更新 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:

我们可能会写下这样的代码:

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 函数。

下面我们来看一下模板编译过程。

1)、首先解析 svelte 模板并生成模板 AST

2)、然后遍历模板 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);

2)、条件逻辑分支会生成独立的子模板构造函数

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();    }  };}

3)、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的调用主要做的是对某个改动的变量进行标记,然后在微任务中调用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 会在各方面不断带给你惊喜。