Svelte 官方博客在 4.30 宣布Svelte 5 进入 RC 状态,这也意味着 Svelte 5 已经达到了一个比较稳定的状态,Svelte 5 对数据响应式的实现进行了重构,带来了更加易用的响应式 API,同时解决了 Svelte 4 实现方式带来的问题。下面我们一起来看看 Svelte 5 带来的响应式特性。
Svelte 4
先来看看在 Svelte 4 中,我们是如何在一个组件中声明响应式数据的:
<script>
let count = 0;
function increment() {
count += 1;
}
$: content = `Total: ${count}`;
</script>
<button on:click="{increment}">Increment</button>
<div>Count: {count}</div>
<div>{content}</div>
在 Svelte 4 中我们可以在组件中通过let
和$
来声明响应式数据/逻辑,在上面的例子中当count
变化之后,content
的值会进行重新计算,同时组件的 UI 也会展示最新的状态。由于 Svelte 4 是通过在编译时进行代码静态分析生成最终的响应式代码,受限于代码静态分析的局限性,在实际使用的时候会有一些边界情况需要我们特别注意:
<script>
let count = 0;
function increment() {
count += 1;
}
function getContent() {
return `Total: ${count}`;
}
$: content = getContent();
</script>
<button on:click="{increment}">Increment</button>
<div>Count: {count}</div>
<div>{content}</div>
如果按照正常的理解上面这个例子的效果应该是和之前一样的,当count
更新之后content
也会相应更新。可是实际上content
并不会更新,之所以不会更新就是因为 Svelte 4 的编译时响应式实现,在编译时进行代码静态分析的时候,编译器并不能识别出content
依赖于count
,因为count
并没有出现在$
表达式中。所以为了让编译器知道content
依赖于count
,必须在$
表达式中显示的表达出来。比如把count
作为getContent
的参数,最终改成$: content = getContent(count)
;
基于编译时的响应式实现带来的另一个问题是:如果我想把上面例子中关于count
的逻辑从组件抽离出来以达到在多个组件之间复用这段逻辑的目的的话,我并不能简单的把代码放到一个单独的文件,然后在组件里面import
这个逻辑。Svelte 4 编译时只会将.svelte
中的变量声明编译成响应式的代码,所以如果我们希望在.svelte
文件以外的地方声明响应式的数据需要使用 Svelte 4 中的Store API:
// counter.js
import { writable } from "svelte/store";
export function createCounter() {
const { subscribe, update } = writable(0);
return {
subscribe,
increment: () => update((n) => n + 1),
};
}
<script>
import { createCounter } from "./counter.js";
const counter = createCouter();
</script>
<button on:click="{counter.increment}">Increment</button>
<div>Count: {$counter}</div>
我们需要通过 Store API 来定义响应式数据,然后在组件中通过$counter
来访问,在编译之后的代码中组件内部会通过调用暴露的subscribe
方法来对counter
进行订阅,当数据变化之后触发视图的更新。
针对上面这些问题,Svelte 5 对底层响应式的实现进行了重构,引入了叫做Runes
的 API.
Runes
Svelte 官方将新的响应式 API 叫做Runes
,准确的说它们并不是 API,而是一系列原语(primitives)或者叫语法糖,最终会被编译成对应的运行时 API。
$state
Svelte 5 中,我们可以通过$state
方法来声明响应式数据:
<script>
let count = $state(0);
let obj = $state({ value: 0 });
function increment() {
count += 1;
obj.value += 1;
}
</script>
<button on:click="{increment}">Increment</button>
<div>Count: {count}, {obj.value}</div>
上面的例子中我们声明了两个变量,一个是基本类型的 state,一个是对象类型的 state,script
部分的代码将会被编译成如下代码:
import * as $ from "svelte/internal/client";
let count = $.state(0);
let obj = $.proxy({ value: 0 });
function increment() {
$.set(count, $.get(count) + 1);
obj.value += 1;
}
可以看到 Svelte 对于两种类型的 state 最终编译的结果是不一样的,我们可以把这里的$.state
和$.proxy
类比到 Vue 中的ref
和reactive
两个 API,在 Vue 中如果要定义一个基本类型的状态,我们需要使用ref
这个 API,在读写的时候需要通过xx.value
来访问和更新状态,而在 Svelte 5 中,框架通过将对基本类型 state 的读写操作编译成$.get(xx)
和$.set(xx)
来达到开发者可以像使用正常变量一样来使用 state 的目的。这里$.state
返回的count
变量其实是跟 Vue 中ref
返回的变量类型一样,是一个对于基础数据类型的一个 wrapper,通过这个 wrapper 框架可以实现对基本类型数据的读写操作的拦截,从而实现依赖收集和数据监听。
对于对象类型的 state 就比较好理解了,目前大多数响应式的实现都是基于浏览器的 Proxy API 实现的依赖收集和数据监听。我们可以发现除了对基本类型数据在编译时进行了易用性的优化,Svelte 5 的响应式实现思路跟 Vue 是一样的。
$derived
$derived
是 Svelte 5 中用来替代$
表达式的语法。我们先来看看例子:
<script>
let count = $state(0);
function increment() {
count += 1;
}
let content = $derived(`hello ${count}`);
</script>
<button on:click="{increment}">Increment</button>
<div>Count: {count}</div>
<div>Content: {content}</div>
$derived
中可以是一个表达式或者是一个函数,函数的返回值将作为$derived
的返回值。上面的$derived
函数最终会被编译成:
$.derived(() => `hello ${$.get(count)}`);
很容易就可以让人联想到 Vue 中的computed
方法,实际上两个函数的作用也是类似的,当count
的值更新之后,content
也会同时更新。
由于$derived
是通过在运行时进行依赖收集的,所以就很好的解决了 Svelte 4 中由于静态代码分析带来的问题,不需要再显示告诉编译器数据依赖关系。
$effect
在 Svelte 4 中$
表达式的另一个使用场景就是当依赖数据发生变更之后执行一些特定逻辑,比如:
<script>
let count = 0;
function increment() {
count += 1;
}
$: {
console.log(count);
}
</script>
<button on:click="{increment}">Increment</button>
<div>Count: {count}</div>
上面这个例子中当count
的值更新之后,$
之后的逻辑都会被执行,这个逻辑就跟 Vue 中的watchEffect
方法是类似的。显然$derived
并不适用于这个场景,所以 Svelte 5 中新增了$effect
语法:
<script>
let count = 0;
function increment() {
count += 1;
}
$effect(() => {
console.log(count);
});
</script>
<button on:click="{increment}">Increment</button>
<div>Count: {count}</div>
$effect
中的函数会在组件mount和count
变化的时候被执行
总结
从Svelte 5 Runes
中的几个API可以发现,Svelte 5通过将响应式是实现从旧版本基于编译时的依赖收集重构到新版本中基于运行时的依赖收集很好的解决了旧版本中的存在的问题;同时由于是基于运行时的响应式,那么也意味着这些Runes
语法可以在任何文件中使用,不仅仅局限于.svelte
文件中,也就是说开发者可以很方便的把组件中的公共逻辑抽离出来在多个组件中进行复用。
更多
除了上面提到的几个Runes
以外,Svelte 5中还提供了很多其他形式的Runes
,详细内容可以查看Svelte 5的最新文档。
另外,本篇文章只是简单的介绍了一些Svelte 5带来的新特性,如果大家对这些特性的底层实现原理感兴趣的话,可以关注后续的文章,会有一系列关于Svelte 4和Svelte 5实现原理的相关文章进行分享。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。