Svelte是什么?

Svelte is a component framework — like React or Vue — but with an important difference.

Svelte的作者Rich Harris(Rollup,Buble的作者)在Svelte 3: Rethinking reactivity这篇文章中如此的介绍,Svelte是一个类似React或者Vue的组件框架。
  
  但是随后也介绍它与React和Vue不同的特性,Rich Harris在Virtual DOM is pure overhead这篇文章中认为:
  1. 虚拟DOM本身是一种性能的开销。
  2. 通过虚拟DOM对比更新DOM也只是一种 ‘通常情况下足够快’的方案。
  3. 通过虚拟DOM的更新需要3步,生成新的虚拟DOM -> 对比新旧虚拟DOM -> 真实DOM的更新,而大部分的更新情况(例如相同的DOM节点只更新了某个属性)只有第三步有价值,因为应用程序的基本结构没有改变。
  4. 虚拟DOM是一种达到声明式、状态驱动UI开发的手段,因为它允许开发者在构建应用程序时无需考虑状态变化,而且性能通常足够好。
  
  Svelte未采用虚拟DOM,而是通过建立数据与真实DOM之间的映射关系,实现数据变化直接更新相关DOM的能力。


语法和基本概念

Svelte采用了模板语法,所以在一些语法特性上与Vue有类似和相通的一些地方。下面通过一些基础的语法和特性来了解一下这个新框架。

使用环境

1. 在线环境。由官方提供的在线编码环境,实时编译结果,还支持查看编译后的代码,有助于增强对Svelte的理解。本文中的示例也都是通过在线环境进行编写。
  2. degit。Svelte 3并未提供官方的脚手架,不过可以通过degit拉取一个Svelte项目的模板。

npx degit sveltejs/template my-svelte-project

拉取成功后通过npm i安装依赖,然后启动项目即可。

组件

组件是一个可重用的、自包含的代码块,将 HTML、CSS 和 JavaScript 代码封装在一起并写入 .svelte 后缀名的文件中即可构成一个组件。
  
  例如在一个App.svelte组件写入如下代码即可构成一个最简单的组件。相比Vue而言,Svelte无需template标签包裹。

<h1>Hello world!</h1>

插值

Svelte采用了单括号 {} 的插值语法,跟React相同。在script代码块中直接声明变量即可在元素标签内进行使用。

<script>
	let name = 'world';
</script>

<h1>Hello {name}!</h1>

CSS样式

Svelte组件的样式是互相隔离的。在style代码块中直接编写样式即可。

<style>
	p {
		color: purple;
		font-family: 'Comic Sans MS', cursive;
		font-size: 2em;
	}
</style>

<p>This is a paragraph.</p>

通过查看编译后的CSS,发现样式会变成带上.svelte-urs9w7这种特殊哈希字符的class。

p.svelte-urs9w7{color:purple;font-family:'Comic Sans MS', cursive;font-size:2em}

而在编译的JS中,发现用来创建页面元素的函数中多了添加css属性的代码:

c() {
	p = element("p");
	p.textContent = "This is a paragraph.";
	//  给P标签添加组件特有标识的class
	attr(p, "class", "svelte-urs9w7");
},

列表渲染

Svelte提供了{#each}…{/each}语句块用来渲染数据列表。在列表渲染这点上,各个框架都各有特色,Vue是通过v-for指令,React则是通过使用React元素数组。

<script>
	let cats = [
		{ id: 'J---aiyznGQ', name: 'Keyboard Cat' },
		{ id: 'z_AbfPXTKms', name: 'Maru' },
		{ id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
	];
</script>

<ul>
	{#each cats as cat, i}
		<li>
			{i + 1}: {cat.name}
		</li>	
	{/each}
</ul>

条件渲染

Svelte提供了{#if}…{/if}语句块用来在模板中做逻辑处理。与条件渲染类似,Vue则是使用v-if指令,而React提供的JSX是JS的扩展,可以直接使用原生语法的逻辑判断语句。

<script>
	let user = { loggedIn: false };

	function toggle() {
		user.loggedIn = !user.loggedIn;
	}
</script>

{#if user.loggedIn}
	<button on:click={toggle}>
		Log out
	</button>
{:else}
	<button on:click={toggle}>
		Log in
	</button>
{/if}

除了{#if},Svelte也同时提供了{:else},{:else if}来完善逻辑语法。

事件

Svelte对于事件的监听十分简单,直接使用on:指令即可。

<script>
	let count = 0;

	function handleClick() {
		count += 1;
	}
</script>

<button on:click={handleClick}>
	Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

响应式

Svelte与React,Vue一样提供了响应式编程的能力,让开发者可以聚焦在数据变化上,而无需操心视图的变化。
  在事件的demo中,count就是一个响应式数据,当count每一次加1, 按钮的文本就会自动更新。
  Svelte使用赋值语句提供响应式能力。相比Vue需要提前在data函数返回值中声明再使用和React需要调用setState来驱动视图更新, Svelte简洁了很多。仅仅只需要正常声明再赋值即可。
  
  Svelte还有更多的语法和API可以自行到官方文档-中文进行查阅和体验,这里就不过多讲解了。


响应式原理的浅析

Svelte既然不采用虚拟DOM,又是提供响应式能力的呢?
  
  Svelte源码分成两部分,compiler 和 runtime。compiler主要负责将Svelte的语法转化成JS代码。
  在这里借用事件一节中demo做示例。在在线环境下,拿到转换后的代码。

/* App.svelte generated by Svelte v3.43.0 */
import {
	SvelteComponent,
	append,
	detach,
	element,
	init,
	insert,
	listen,
	noop,
	safe_not_equal,
	set_data,
	space,
	text
} from "svelte/internal";

//  用于构建页面元素和处理元素行为的函数
function create_fragment(ctx) {
	let button;
	let t0;
	let t1;
	let t2;
	let t3_value = (/*count*/ ctx[0] === 1 ? 'time' : 'times') + "";
	let t3;
	let mounted;
	let dispose;

	return {
	    // created函数   用于创建元素
		c() {
			button = element("button");
			t0 = text("Clicked ");
			t1 = text(/*count*/ ctx[0]);
			t2 = space();
			t3 = text(t3_value);
		},
		// mounted函数 用于处理创建的元素在页面中的行为
		m(target, anchor) {
			insert(target, button, anchor);
			append(button, t0);
			append(button, t1);
			append(button, t2);
			append(button, t3);
            
            //  挂载事件
			if (!mounted) {
				dispose = listen(button, "click", /*handleClick*/ ctx[1]);
				mounted = true;
			}
		},
		// update函数  用于更新元素
		p(ctx, [dirty]) {
			if (dirty & /*count*/ 1) set_data(t1, /*count*/ ctx[0]);
			if (dirty & /*count*/ 1 && t3_value !== (t3_value = (/*count*/ ctx[0] === 1 ? 'time' : 'times') + "")) set_data(t3, t3_value);
		},
		i: noop,
		o: noop,
        // destory函数 用来处理元素销毁时的行为		
		d(detaching) {
			if (detaching) detach(button);
			mounted = false;
			dispose();
		}
	};
}

// 用于管理script代码块中声明的变量和函数,提供了组件的上下文
function instance($$self, $$props, $$invalidate) {
	let count = 0;

	function handleClick() {
	    // 赋值语句被封装,用来处理响应式
		$$invalidate(0, count += 1);
	}

	return [count, handleClick];
}

class App extends SvelteComponent {
	constructor(options) {
		super();
		// 组件创建的入口
		init(this, options, instance, create_fragment, safe_not_equal, {});
	}
}

export default App;

转换后的结构

转换后的代码主要分为三部分。主要是 create_fragment 函数,instance 函数,App 类。
  
  create_fragment函数返回了一个对象,里面主要包含了如何创建页面元素(c函数)、如何构建和组织页面元素(m函数)以及如何更新页面元素(p函数)的方法。
  
  instance 函数主要是转换了 script 代码块里的 变量以及行为。
  
  App 类主要就是继承了SvelteComponent同时调用了init方法。

整体流程

Svelte的整体流程并不复杂。组件会被转成原生的Class。入口文件会调用这个类。例如最简单的挂载到页面上仅仅只需要一个容器(DOM元素)。

const app = new App({
	target: document.body,
});

随后会init方法会调用create_fragment函数,拿到组件的各个生命周期函数。调用c函数创建页面元素,根据配置执行一些内置函数,主要是生成一些属性和执行生命周期钩子。以及在Svelte调度器中推入一些内置的callback,最后通过调用m函数来完成页面元素的挂载。

响应式原理

在官方文档中提到,Svelte 其实是将赋值语句替换为一些代码,这些代码将通知 Svelte 更新 DOM。
  
  通过查看转换后的JS代码。也确实发现赋值语句会被替换成一个名为$$invalidate的函数。
  再通过翻查源码 runtime 中的函数发现。$$invalidate是一个在init阶段传递的匿名函数。

// instance就是instance函数,$$invalidate就是匿名的箭头函数。

// ctx就是instance的返回值。可以认为ctx为组件的上下文  里面包含了变量以及函数的行为。

//  i 对应了 此次赋值变量的位置,例如count在ctx中位置为0。handleClick中$$invalidate的传参第一个参数就是0。
$$.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;
    })
    : [];

当值发生变化时并且组件处于ready阶段(前置工作已完成,例如更新current_component指针,$$属性初始化等等)。$$invalidate被调用。执行 make_dirty 方法。源码如下:

// 用于标记脏组件(通知哪个组件更新),脏数据(标记需要更新的数据),以及在微任务队列中推入flush方法。
function make_dirty(component, i) {
	if (component.$$.dirty[0] === -1) {
		dirty_components.push(component);
		// 延迟调用flush方法	
		schedule_update();
		component.$$.dirty.fill(0);
	}
	// 更新标记  dirty根据i生成二进制标记
	// 通过dirty中的标记可以知道需要执行p函数中哪些更新逻辑  
	component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
}

在 make_dirty 方法中, Svelte通过二进制标志位对脏数据进行记录。
  
  例如count - ctx (instance函数返回值) 中索引第一个变量 进行了修改:
  1. component被推入到dirty_components中,
  2. schedule_update()在微任务中推入flush(用来执行调度器内所有任务队列的函数)
  3. component.$$.dirty[0]的标记从-1被重置到0 ,然后通过索引计算结果得到标记为1。
  4. 当flush执行时,会调用组件的更新函数(p函数),而p函数根据dirty标记与在编译阶段就得到的映射(常量数值)进行 按位与 & 就可以知道哪一部分可以执行set_data。

Svelte的响应式原理意味着无法侦测到数组通过push,pop等方式的修改,必须通过赋值方式修改数组和对象。


Svelte,React,Vue对于常用语法或特性的支持程度与一些差异

Svelte React Vue
描述ui 模板 JSX 模板或JSX
组件 支持 支持 支持(需额外在component中注入)
动态属性 attr= attr= attr={{attr}}
样式隔离 支持 支持 支持
响应式能力 支持 支持 支持
Props 支持 支持 支持
条件渲染 原生JS的条件判断 v-if
列表渲染 React元素数组 v-for
事件绑定 on: on+[事件名]属性 v-on
动态class 支持 支持 支持
绑定数据 bind: 不支持 v-bind
Context 支持 支持 提供类似作用的provide/inject
Slot 支持 不支持,但可通过Props直接传递组件或元素 支持
生命周期函数 支持 支持 支持

可以看到,三个框架基本都支持在上述列到的常用语法或者提供了类似的语法。


性能

A RealWorld Comparison of Front-End Frameworks 2020的测评报告中,有三个衡量指标:
  1.Performance 性能

image

通过chrome的性能面板比较,Svlete的得分已经拿到了并列第一的名次。而React,Vue应用的性能得分已经处于中游了。
  2.Size 大小

image-1652944748946
  而在包大小的方面,通过Gzip压缩后的包仅仅只有15kb,直接拿到第一的位置。不过这也是因为Svelte消除了运行时,打包产物存在大量重复的构建节点等代码,这也让打包后的代码在Gzip中极其受益。

3.Lines of Code 代码行数
  image-1652944757935
而完成相同的应用,使用Svlete的代码量也是远远的少于其他框架。

同时,翻看了2019年的报告,对比过后,Svelte在包大小和代码行数上依旧保持着领先,这得益于它的核心思想(通过静态编译减少运行时),但是在性能上经过一年有了翻天覆地的变化,直接从中游的位置来到了第一的位置。

不仅如此,另外一个前端框架性能对比的项目也给出了同样的答案。这足以证明Svelte在性能、体积上具有强大的优势。

生态环境

Svelte React Vue
TS 支持 支持 支持
状态管理 自带 Redux/Mobx等等 Vuex
UI Material UI 太多了 太多了
路由 Svelte-router React-router Vue-router
服务端渲染 支持 支持 支持
测试工具 官网无相关内容 Jest Vue Test Utils
脚手架 create-react-app vue-cli
在生态环境这方面,Svelte呈现了明显的劣势,在国内没有成熟的生产环境的ui库,不过这也是因为Svelte诞生较晚,而早期的版本优势也并不明显。

总结

在React,Vue两个成熟且生态环境十分完善的框架盛行的趋势下,Svelte作为一个新兴的前端框架,以一个完全不同的设计思路实现了响应式的编程范式,不管是性能,还是语法的完善度和易上手程度都是十分可观的,但是构建项目不仅仅只是考虑框架的可行性,个人看来,Svelte的生态环境还不足以直接支撑大型项目的落地,在国内缺少成熟的UI库,脚手架工具的缺少也意味着Svelte在集成这一块的薄弱性,同时Svelte的原理意味着修改数据的方式并没有那么贴近原生语法,依赖赋值语句。
  总的来说,Svelte相对于其他框架的优势在项目生产力上并不明显。在编程思想上也仅仅只是响应式的另一种具现。不过基于它的特性和性能以及现有的生态环境,感兴趣的朋友还是值得一试。